1use std::fmt;
3use std::ops;
4
5use data_encoding::BASE64;
6use serde::de::{self, Visitor};
7use serde::{Deserialize, Serialize};
8
9use crate::repo_commits::CommitDetails;
10use crate::utils::{percent_encode, PATH};
11use crate::{Future, Github, Stream};
12
13pub struct Content {
15 github: Github,
16 owner: String,
17 repo: String,
18}
19
20impl Content {
21 #[doc(hidden)]
22 pub fn new<O, R>(github: Github, owner: O, repo: R) -> Self
23 where
24 O: Into<String>,
25 R: Into<String>,
26 {
27 Content {
28 github,
29 owner: owner.into(),
30 repo: repo.into(),
31 }
32 }
33
34 fn path(&self, location: &str, ref_: &str) -> String {
35 let location = percent_encode(location.as_ref(), PATH);
38 let mut path = format!("/repos/{}/{}/contents{}", self.owner, self.repo, location);
39 if !ref_.is_empty() {
40 path += &format!("?ref={}", ref_);
41 }
42 path
43 }
44
45 pub fn get(&self, location: &str, ref_: &str) -> Future<Contents> {
48 self.github.get(&self.path(location, ref_))
49 }
50
51 pub fn file(&self, location: &str, ref_: &str) -> Future<File> {
56 self.github.get(&self.path(location, ref_))
57 }
58
59 pub fn root(&self, ref_: &str) -> Stream<DirectoryItem> {
61 self.iter("/", ref_)
62 }
63
64 pub fn iter(&self, location: &str, ref_: &str) -> Stream<DirectoryItem> {
69 self.github.get_stream(&self.path(location, ref_))
70 }
71
72 pub fn create(&self, location: &str, content: &[u8], message: &str) -> Future<NewFileResponse> {
75 let file = &NewFile {
76 content: BASE64.encode(content),
77 message: message.to_string(),
78 sha: None,
79 };
80 self.github.put(&self.path(location, ""), json!(file))
81 }
82
83 pub fn update(
86 &self,
87 location: &str,
88 content: &[u8],
89 message: &str,
90 sha: &str,
91 ) -> Future<NewFileResponse> {
92 let file = &NewFile {
93 content: BASE64.encode(content),
94 message: message.to_string(),
95 sha: Some(sha.to_string()),
96 };
97 self.github.put(&self.path(location, ""), json!(file))
98 }
99
100 pub fn delete(&self, location: &str, message: &str, sha: &str) -> Future<()> {
102 let file = &NewFile {
103 content: "".to_string(),
104 message: message.to_string(),
105 sha: Some(sha.to_string()),
106 };
107 self.github
108 .delete_message(&self.path(location, ""), json!(file))
109 }
110}
111
112#[derive(Debug, Deserialize)]
114#[serde(rename_all = "snake_case", tag = "type")]
115pub enum Contents {
116 File(File),
117 Symlink(Symlink),
118 Submodule(Submodule),
119}
120
121#[derive(Debug, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum Encoding {
125 Base64,
126 }
128
129#[derive(Debug, Serialize)]
130pub struct NewFile {
131 #[serde(skip_serializing_if = "String::is_empty")]
132 pub content: String,
133 pub message: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub sha: Option<String>,
136}
137
138#[derive(Debug, Deserialize)]
139pub struct NewFileResponse {
140 pub commit: CommitDetails,
141}
142
143#[derive(Debug, Deserialize)]
144pub struct File {
145 pub encoding: Encoding,
146 pub size: u32,
147 pub name: String,
148 pub path: String,
149 pub content: DecodedContents,
150 pub sha: String,
151 pub url: String,
152 pub git_url: String,
153 pub html_url: String,
154 pub download_url: String,
155 pub _links: Links,
156}
157
158#[derive(Debug, Deserialize)]
159pub struct DirectoryItem {
160 #[serde(rename = "type")]
161 pub _type: String,
162 pub size: u32,
163 pub name: String,
164 pub path: String,
165 pub sha: String,
166 pub url: String,
167 pub git_url: String,
168 pub html_url: String,
169 pub download_url: Option<String>,
170 pub _links: Links,
171}
172
173#[derive(Debug, Deserialize)]
174pub struct Symlink {
175 pub target: String,
176 pub size: u32,
177 pub name: String,
178 pub path: String,
179 pub sha: String,
180 pub url: String,
181 pub git_url: String,
182 pub html_url: String,
183 pub download_url: String,
184 pub _links: Links,
185}
186
187#[derive(Debug, Deserialize)]
188pub struct Submodule {
189 pub submodule_git_url: String,
190 pub size: u32,
191 pub name: String,
192 pub path: String,
193 pub sha: String,
194 pub url: String,
195 pub git_url: String,
196 pub html_url: String,
197 pub download_url: Option<String>,
198 pub _links: Links,
199}
200
201#[derive(Debug, Deserialize)]
202pub struct Links {
203 pub git: String,
204 #[serde(rename = "self")]
205 pub _self: String,
206 pub html: String,
207}
208
209#[derive(Debug)]
211pub struct DecodedContents(Vec<u8>);
212
213impl Into<Vec<u8>> for DecodedContents {
214 fn into(self) -> Vec<u8> {
215 self.0
216 }
217}
218
219impl AsRef<[u8]> for DecodedContents {
220 fn as_ref(&self) -> &[u8] {
221 &self.0
222 }
223}
224
225impl ops::Deref for DecodedContents {
226 type Target = [u8];
227
228 fn deref(&self) -> &Self::Target {
229 &self.0
230 }
231}
232
233impl<'de> Deserialize<'de> for DecodedContents {
234 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
235 where
236 D: serde::Deserializer<'de>,
237 {
238 struct DecodedContentsVisitor;
239
240 impl<'de> Visitor<'de> for DecodedContentsVisitor {
241 type Value = DecodedContents;
242
243 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 write!(f, "base64 string")
245 }
246
247 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
248 where
249 E: de::Error,
250 {
251 let v = v.replace("\n", "");
255
256 let decoded = base64::decode_config(&v, base64::STANDARD).map_err(|e| match e {
257 base64::DecodeError::InvalidLength => {
258 E::invalid_length(v.len(), &"invalid base64 length")
259 }
260 base64::DecodeError::InvalidByte(offset, byte) => E::invalid_value(
261 de::Unexpected::Bytes(&[byte]),
262 &format!("valid base64 character at offset {}", offset).as_str(),
263 ),
264 base64::DecodeError::InvalidLastSymbol(offset, byte) => E::invalid_value(
265 de::Unexpected::Bytes(&[byte]),
266 &format!("valid last base64 character at offset {}", offset).as_str(),
267 ),
268 })?;
269
270 Ok(DecodedContents(decoded))
271 }
272 }
273
274 deserializer.deserialize_str(DecodedContentsVisitor)
275 }
276}