Merge pull request 'Blob support' (#30) from get_blob into main

Reviewed-on: #30
This commit is contained in:
glyph 2022-02-26 12:13:14 +00:00
commit 7b78274a52
6 changed files with 261 additions and 2 deletions

66
Cargo.lock generated
View File

@ -223,6 +223,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "blocking"
version = "1.1.0"
@ -282,6 +291,15 @@ dependencies = [
"cache-padded",
]
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.7"
@ -292,6 +310,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "ctor"
version = "0.1.21"
@ -302,6 +330,16 @@ dependencies = [
"syn",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dirs"
version = "2.0.2"
@ -448,6 +486,16 @@ version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "get_if_addrs"
version = "0.5.3"
@ -507,6 +555,7 @@ dependencies = [
"kuska-ssb",
"serde",
"serde_json",
"sha2",
]
[[package]]
@ -814,6 +863,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook"
version = "0.3.13"
@ -880,6 +940,12 @@ dependencies = [
"syn",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-xid"
version = "0.2.2"

View File

@ -22,3 +22,4 @@ kuska-sodiumoxide = "0.2.5-0"
kuska-ssb = "0.4.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10.2"

View File

@ -4,9 +4,12 @@
//!
//! - [`Sbot::publish`]
//! - [`Sbot::publish_description`]
//! - [`Sbot::publish_image`]
//! - [`Sbot::publish_name`]
//! - [`Sbot::publish_post`]
use kuska_ssb::api::dto::content::Image;
use crate::{error::GolgiError, messages::SsbMessageContent, sbot::Sbot, utils};
impl Sbot {
@ -116,6 +119,45 @@ impl Sbot {
self.publish(msg).await
}
/// Publish an image for the local identity.
///
/// Requires the image to have been added to the local blobstore. The
/// ID of the blob (sigil-link in the form `&...=.sha256`) must be provided.
///
/// Convenient wrapper around the `publish` method which constructs and
/// publishes an `about` type name message appropriately from a string.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError};
///
/// async fn publish_an_image() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(None, None).await?;
///
/// let blob_id = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
///
/// let msg_ref = sbot_client.publish_image(blob_id).await?;
///
/// println!("msg reference: {}", msg_ref);
///
/// Ok(())
/// }
/// ```
pub async fn publish_image(&mut self, blob_id: &str) -> Result<String, GolgiError> {
let msg = SsbMessageContent::About {
about: self.id.to_string(),
name: None,
title: None,
branch: None,
image: Some(Image::OnlyLink(blob_id.to_string())),
description: None,
location: None,
start_datetime: None,
};
self.publish(msg).await
}
/// Publish a name for the local identity.
///
/// Convenient wrapper around the `publish` method which constructs and

145
src/blobs.rs Normal file
View File

@ -0,0 +1,145 @@
//! Blob utilities which do not require RPC calls.
//!
//! Offers the following functions:
//!
//! - [`get_blob_path()`]
//! - [`hash_blob()`]
use base64;
use sha2::{Digest, Sha256};
use crate::error::GolgiError;
/// Lookup the filepath for a blob.
///
/// # Example
///
/// ```rust
/// use golgi::{blobs, GolgiError};
///
/// fn lookup_blob_path() -> Result<(), GolgiError> {
/// let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
///
/// let blob_path = blobs::get_blob_path(blob_ref)?;
///
/// println!("{}", blob_path);
///
/// // 26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f
///
/// Ok(())
/// }
/// ```
pub fn get_blob_path(blob_id: &str) -> Result<String, GolgiError> {
// ensure the id starts with the correct sigil link
if !blob_id.starts_with('&') {
return Err(GolgiError::SigilLink(format!(
"incorrect first character, expected '&' sigil: {}",
blob_id
)));
}
// find the dot index denoting the start of the algorithm definition tag
let dot_index = blob_id
.rfind('.')
.ok_or_else(|| GolgiError::SigilLink(format!("no dot index was found: {}", blob_id)))?;
// obtain the base64 portion (substring) of the blob id
let base64_str = &blob_id[1..dot_index];
// decode blob substring from base64 (to bytes)
let blob_bytes = base64::decode_config(base64_str, base64::STANDARD)?;
// represent the blob bytes as hex, removing all unnecessary characters
let blob_hex = format!("{:02x?}", blob_bytes)
.replace('[', "")
.replace(']', "")
.replace(',', "")
.replace(' ', "");
// split the hex representation of the decoded base64
// this is how paths are formatted for the blobstore
// e.g. 26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f
// full path would be: `/home/user/.ssb-go/blobs/sha256/26/524773dc...`
let blob_path = format!("{}/{}", &blob_hex[..2], &blob_hex[2..]);
Ok(blob_path)
}
/// Hash the given blob byte slice and return the hex representation and
/// blob ID (sigil-link).
///
/// This function can be used when adding a blob to the local blobstore.
pub fn hash_blob(buffer: &[u8]) -> Result<(String, String), GolgiError> {
// hash the bytes
let hash = Sha256::digest(buffer);
// generate a hex representation of the hash
let hex_hash = format!("{:02x?}", hash)
.replace('[', "")
.replace(']', "")
.replace(',', "")
.replace(' ', "");
// encode the hash
let b64_hash = base64::encode(&hash[..]);
// format the base64 encoding as a blob sigil-link (blob id)
let blob_id = format!("&{}.sha256", b64_hash);
Ok((hex_hash, blob_id))
}
#[cfg(test)]
mod tests {
use crate::blobs;
/* HAPPY PATH TESTS */
#[test]
fn blob_path() {
let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
let blob_path = blobs::get_blob_path(blob_ref);
assert!(blob_path.is_ok());
assert_eq!(
blob_path.unwrap(),
"26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f"
);
}
#[test]
fn hashed_blob() {
// pretend this represents a file which has been written to a buffer
let buffer = vec![7; 128];
let hash_result = blobs::hash_blob(&buffer);
assert!(hash_result.is_ok());
let (hex_hash, blob_id) = hash_result.unwrap();
assert_eq!(
hex_hash,
"4c1398e54d53e925cff04da532f4bbaf15f75b5981fc76c2072dfdc6491a9fb1"
);
assert_eq!(
blob_id,
"&TBOY5U1T6SXP8E2lMvS7rxX3W1mB/HbCBy39xkkan7E=.sha256"
);
}
/* SAD PATH TESTS */
#[test]
fn blob_id_without_sigil() {
let blob_ref = "JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256";
match blobs::get_blob_path(blob_ref) {
Err(e) => assert_eq!(e.to_string(), "SSB blob ID error: incorrect first character, expected '&' sigil: JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256"),
_ => panic!(),
}
}
#[test]
fn blob_id_without_algo() {
let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=";
match blobs::get_blob_path(blob_ref) {
Err(e) => assert_eq!(e.to_string(), "SSB blob ID error: no dot index was found: &JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8="),
_ => panic!(),
}
}
}

View File

@ -34,9 +34,11 @@ pub enum GolgiError {
Rpc(RpcError),
/// Go-sbot error.
Sbot(String),
/// SSB sigil-link error.
SigilLink(String),
/// JSON serialization or deserialization error.
SerdeJson(JsonError),
/// Error decoding typed ssb message from content.
/// Error decoding typed SSB message from content.
ContentType(String),
/// Error decoding UTF8 string from bytes
Utf8Parse {
@ -55,6 +57,7 @@ impl std::error::Error for GolgiError {
GolgiError::Feed(ref err) => Some(err),
GolgiError::Rpc(ref err) => Some(err),
GolgiError::Sbot(_) => None,
GolgiError::SigilLink(_) => None,
GolgiError::SerdeJson(ref err) => Some(err),
GolgiError::ContentType(_) => None,
GolgiError::Utf8Parse { ref source } => Some(source),
@ -75,11 +78,12 @@ impl std::fmt::Display for GolgiError {
// then have the core display msg be: "SSB RPC error: {}", context
GolgiError::Rpc(ref err) => write!(f, "SSB RPC failure: {}", err),
GolgiError::Sbot(ref err) => write!(f, "Sbot returned an error response: {}", err),
GolgiError::SigilLink(ref context) => write!(f, "SSB blob ID error: {}", context),
GolgiError::SerdeJson(_) => write!(f, "Failed to serialize JSON slice"),
//GolgiError::WhoAmI(ref err) => write!(f, "{}", err),
GolgiError::ContentType(ref err) => write!(
f,
"Failed to decode typed message from ssb message content: {}",
"Failed to decode typed message from SSB message content: {}",
err
),
GolgiError::Utf8Parse { source } => {

View File

@ -69,6 +69,7 @@
//! ```
pub mod api;
pub mod blobs;
pub mod error;
pub mod messages;
pub mod sbot;