use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::PodError;
use crate::storage::Storage;
pub const EXPORT_JSONLD_CONTEXT: &str = "https://solid-pod-rs.dev/ns/export/v1";
pub const EXPORT_CONTENT_TYPE: &str = "application/ld+json";
pub const PRIVATE_CONTAINER_PREFIX: &str = "/private/";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodExportEntry {
pub path: String,
pub content_type: String,
pub etag: String,
pub created: DateTime<Utc>,
pub modified: DateTime<Utc>,
pub body_base64: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodExportBundle {
#[serde(rename = "@context")]
pub context: String,
pub pod_base: String,
pub generated_at: DateTime<Utc>,
pub includes_private: bool,
pub entries: Vec<PodExportEntry>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ExportOptions {
#[serde(default)]
pub include_private: bool,
}
fn join(parent: &str, child: &str) -> String {
let mut joined = String::with_capacity(parent.len() + child.len() + 1);
joined.push_str(parent);
if !parent.ends_with('/') {
joined.push('/');
}
joined.push_str(child);
joined
}
async fn walk_resources<S: Storage + ?Sized>(
storage: &S,
include_private: bool,
) -> Result<Vec<String>, PodError> {
let mut stack: Vec<String> = vec!["/".to_string()];
let mut resources: Vec<String> = Vec::new();
while let Some(container) = stack.pop() {
if !include_private && container.starts_with(PRIVATE_CONTAINER_PREFIX) {
continue;
}
let children = match storage.list(&container).await {
Ok(v) => v,
Err(PodError::NotFound(_)) => continue,
Err(e) => return Err(e),
};
for child in children {
let abs = join(&container, &child);
if !include_private && abs.starts_with(PRIVATE_CONTAINER_PREFIX) {
continue;
}
if abs.ends_with('/') {
stack.push(abs);
} else {
resources.push(abs);
}
}
}
Ok(resources)
}
pub async fn export_pod_jsonld<S: Storage + ?Sized>(
storage: &S,
pod_base: &str,
options: ExportOptions,
) -> Result<PodExportBundle, PodError> {
let paths = walk_resources(storage, options.include_private).await?;
let mut entries: Vec<PodExportEntry> = Vec::with_capacity(paths.len());
for path in paths {
let (body, meta) = match storage.get(&path).await {
Ok(v) => v,
Err(PodError::NotFound(_)) => continue,
Err(e) => return Err(e),
};
entries.push(PodExportEntry {
path,
content_type: meta.content_type,
etag: meta.etag,
created: meta.modified,
modified: meta.modified,
body_base64: BASE64_STANDARD.encode(&body),
});
}
entries.sort_by(|a, b| a.created.cmp(&b.created));
Ok(PodExportBundle {
context: EXPORT_JSONLD_CONTEXT.to_string(),
pod_base: pod_base.to_string(),
generated_at: Utc::now(),
includes_private: options.include_private,
entries,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_handles_trailing_slash() {
assert_eq!(join("/", "foo"), "/foo");
assert_eq!(join("/dir/", "foo"), "/dir/foo");
assert_eq!(join("/dir", "foo"), "/dir/foo");
assert_eq!(join("/dir/", "sub/"), "/dir/sub/");
}
}