This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-lfs / src / batch.rs
6.7 kB 222 lines
1use serde::{Deserialize, Deserializer, Serialize, Serializer}; 2use url::Url; 3 4use knot_types::{HttpStatus, RefName}; 5 6use crate::{ClaimedSize, LfsOid}; 7 8pub const BATCH_MEDIA_TYPE: &str = "application/vnd.git-lfs+json"; 9pub const HASH_ALGO: &str = "sha256"; 10pub const BASIC_TRANSFER: &str = "basic"; 11pub const MAX_BATCH_OBJECTS: usize = 1000; 12 13// Unknown adapters parse instead of failing the whole body, 14// so that a client offering something we don't serve 15// will at least get a 422 that shows a mismatch not a serde error! 16#[derive(Debug, Clone, PartialEq, Eq)] 17pub enum TransferAdapter { 18 Basic, 19 Other(String), 20} 21 22impl TransferAdapter { 23 pub fn is_basic(&self) -> bool { 24 matches!(self, Self::Basic) 25 } 26 27 pub fn as_str(&self) -> &str { 28 match self { 29 Self::Basic => BASIC_TRANSFER, 30 Self::Other(value) => value, 31 } 32 } 33} 34 35impl Serialize for TransferAdapter { 36 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { 37 serializer.serialize_str(self.as_str()) 38 } 39} 40 41impl<'de> Deserialize<'de> for TransferAdapter { 42 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { 43 let raw = String::deserialize(deserializer)?; 44 Ok(if raw == BASIC_TRANSFER { 45 Self::Basic 46 } else { 47 Self::Other(raw) 48 }) 49 } 50} 51 52#[derive(Debug, Clone, PartialEq, Eq)] 53pub enum HashAlgo { 54 Sha256, 55 Other(String), 56} 57 58impl HashAlgo { 59 pub fn is_sha256(&self) -> bool { 60 matches!(self, Self::Sha256) 61 } 62 63 pub fn as_str(&self) -> &str { 64 match self { 65 Self::Sha256 => HASH_ALGO, 66 Self::Other(value) => value, 67 } 68 } 69} 70 71impl Serialize for HashAlgo { 72 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { 73 serializer.serialize_str(self.as_str()) 74 } 75} 76 77impl<'de> Deserialize<'de> for HashAlgo { 78 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { 79 let raw = String::deserialize(deserializer)?; 80 Ok(if raw == HASH_ALGO { 81 Self::Sha256 82 } else { 83 Self::Other(raw) 84 }) 85 } 86} 87 88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 89#[serde(rename_all = "lowercase")] 90pub enum BatchOperation { 91 Download, 92 Upload, 93} 94 95#[derive(Debug, Clone, Serialize, Deserialize)] 96pub struct BatchRef { 97 pub name: RefName, 98} 99 100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 101pub struct BatchObject { 102 pub oid: LfsOid, 103 pub size: ClaimedSize, 104} 105 106#[derive(Debug, Clone, Serialize, Deserialize)] 107pub struct BatchRequest { 108 pub operation: BatchOperation, 109 #[serde(default, skip_serializing_if = "Vec::is_empty")] 110 pub transfers: Vec<TransferAdapter>, 111 #[serde(default, rename = "ref", skip_serializing_if = "Option::is_none")] 112 pub reference: Option<BatchRef>, 113 pub objects: Vec<BatchObject>, 114 #[serde(default, skip_serializing_if = "Option::is_none")] 115 pub hash_algo: Option<HashAlgo>, 116} 117 118#[derive(Debug, Clone, Serialize, Deserialize)] 119pub struct BatchAction { 120 pub href: Url, 121} 122 123#[derive(Debug, Clone, Serialize, Deserialize)] 124pub struct BatchActions { 125 #[serde(default, skip_serializing_if = "Option::is_none")] 126 pub download: Option<BatchAction>, 127 #[serde(default, skip_serializing_if = "Option::is_none")] 128 pub upload: Option<BatchAction>, 129} 130 131#[derive(Debug, Clone, Serialize, Deserialize)] 132pub struct BatchObjectError { 133 pub code: HttpStatus, 134 pub message: String, 135} 136 137#[derive(Debug, Clone, Serialize, Deserialize)] 138pub struct BatchResponseObject { 139 pub oid: LfsOid, 140 pub size: ClaimedSize, 141 #[serde(default, skip_serializing_if = "Option::is_none")] 142 pub authenticated: Option<bool>, 143 #[serde(default, skip_serializing_if = "Option::is_none")] 144 pub actions: Option<BatchActions>, 145 #[serde(default, skip_serializing_if = "Option::is_none")] 146 pub error: Option<BatchObjectError>, 147} 148 149fn basic_transfer() -> TransferAdapter { 150 TransferAdapter::Basic 151} 152 153#[derive(Debug, Clone, Serialize, Deserialize)] 154pub struct BatchResponse { 155 #[serde(default = "basic_transfer")] 156 pub transfer: TransferAdapter, 157 pub objects: Vec<BatchResponseObject>, 158 #[serde(default, skip_serializing_if = "Option::is_none")] 159 pub hash_algo: Option<HashAlgo>, 160} 161 162#[cfg(test)] 163mod tests { 164 use super::*; 165 166 const OID: &str = "6c17f2007cbe934aee6e309b28b2fba3c119d98be6ea4156da3aa3173456ad16"; 167 168 #[test] 169 fn a_client_batch_request_round_trips() { 170 let body = format!( 171 r#"{{"operation":"download","transfers":["basic","ssh"],"ref":{{"name":"refs/heads/main"}},"objects":[{{"oid":"{OID}","size":42}}],"hash_algo":"sha256"}}"# 172 ); 173 let request: BatchRequest = serde_json::from_str(&body).unwrap(); 174 assert_eq!(request.operation, BatchOperation::Download); 175 assert_eq!(request.objects[0].oid.as_str(), OID); 176 assert_eq!(request.objects[0].size, ClaimedSize::new(42)); 177 assert_eq!( 178 request.reference.as_ref().unwrap().name.as_str(), 179 "refs/heads/main" 180 ); 181 } 182 183 #[test] 184 fn a_hostile_oid_fails_the_whole_parse() { 185 let body = r#"{"operation":"download","objects":[{"oid":"../../etc/passwd","size":1}]}"#; 186 assert!(serde_json::from_str::<BatchRequest>(body).is_err()); 187 } 188 189 #[test] 190 fn a_response_serializes_the_lfs_shape() { 191 let response = BatchResponse { 192 transfer: TransferAdapter::Basic, 193 objects: vec![BatchResponseObject { 194 oid: LfsOid::new(OID).unwrap(), 195 size: ClaimedSize::new(42), 196 authenticated: Some(true), 197 actions: Some(BatchActions { 198 download: Some(BatchAction { 199 href: Url::parse(&format!( 200 "https://nel.pet/did:plc:squid/media/info/lfs/objects/{OID}" 201 )) 202 .unwrap(), 203 }), 204 upload: None, 205 }), 206 error: None, 207 }], 208 hash_algo: Some(HashAlgo::Sha256), 209 }; 210 let json = serde_json::to_value(&response).unwrap(); 211 assert_eq!(json["transfer"], "basic"); 212 assert_eq!(json["objects"][0]["oid"], OID); 213 assert_eq!(json["objects"][0]["authenticated"], true); 214 assert!( 215 json["objects"][0]["actions"]["download"]["href"] 216 .as_str() 217 .unwrap() 218 .ends_with(OID) 219 ); 220 assert_eq!(json["objects"][0].get("error"), None); 221 } 222}