neoercities/lib.rs
1//! Another simple Neocities API wrapper.
2//!
3//! # Usage:
4//!
5//! Create a [`NeocitiesClient`] either with or without authentication
6//! (no-auth clients have very limited functionality).
7//!
8//! ```no_run
9//! let client1 = NeocitiesClient::new("randomuser", "notmypassword");
10//! let client2 = NeocitiesClient::new_with_key(&key);
11//! let client3 = NeocitiesClient::new_no_auth();
12//! ```
13//!
14//! From there, you can talk to the Neocities API at your leisure.
15//!
16//! ```no_run
17//! let info = client1.info();
18//! client2.upload("site/file.txt", "file.txt");
19//! let someone_elses_info = client3.info_no_auth("ambyshframber");
20//! // this is the only method that no-auth clients can call
21//! ```
22//!
23//! The crate also includes an optional utility module for dealing with site file lists. Enable the `site_info` feature to use it.
24
25use std::path::Path;
26use std::fs::read;
27
28use reqwest::{blocking::{Client, RequestBuilder, multipart::{Part, Form}}};
29use thiserror::Error;
30
31#[cfg(any(feature = "site_info", doc))]
32pub mod site_info;
33
34/// The API client.
35///
36/// Can be created with authentication (username/password or API key) or without authentication.
37/// Clients without authentication can only use `info_no_auth()`,
38/// and all other methods will return an error.
39///
40/// All methods should return valid JSON.
41/// Check beforehand though, because something might go wrong at the remote end.
42#[derive(Debug)]
43pub struct NeocitiesClient {
44 client: Client,
45 has_auth: bool,
46 username: String,
47 password: String,
48 api_key: Option<String>
49}
50
51impl NeocitiesClient {
52 /// Creates a client with a username and password.
53 /// API methods called on the client will relate to the website belonging to the auth user.
54 ///
55 /// Using a username and password is not recommended for automated tasks,
56 /// as that involves leaving plaintext passwords in source code or configuration files.
57 pub fn new(username: &str, password: &str) -> NeocitiesClient {
58 NeocitiesClient {
59 client: Client::new(),
60 has_auth: true,
61 username: String::from(username),
62 password: String::from(password),
63 api_key: None
64 }
65 }
66 /// Creates a client with an API key.
67 /// API methods called on the client will relate to the website belonging to the auth user.
68 ///
69 /// ```no_run
70 /// # use rs_neocities::client::NeocitiesClient;
71 /// # use std::fs;
72 /// let key = fs::read_to_string("auth_key.txt")?;
73 /// let c = NeocitiesClient::new_with_key(&key);
74 /// assert!(c.info().is_ok());
75 /// ```
76 ///
77 /// A key can be obtained by creating a client with a username and password,
78 /// and calling `get_key()`. Keep it somewhere secure!
79 pub fn new_with_key(key: &str) -> NeocitiesClient {
80 NeocitiesClient {
81 client: Client::new(),
82 has_auth: true,
83 username: String::new(),
84 password: String::new(),
85 api_key: Some(String::from(key))
86 }
87 }
88 /// Creates a client with no authentication.
89 ///
90 /// Calls to methods other than `info_no_auth()` will return an error.
91 ///
92 /// ```no_run
93 /// # use rs_neocities::client::NeocitiesClient;
94 /// let c = NeocitiesClient::new_no_auth();
95 /// assert!(c.info_no_auth("ambyshframber").is_ok());
96 /// assert!(c.list_all().is_err())
97 /// ```
98 pub fn new_no_auth() -> NeocitiesClient {
99 NeocitiesClient {
100 client: Client::new(),
101 has_auth: false,
102 username: String::new(),
103 password: String::new(),
104 api_key: None
105 }
106 }
107
108 fn get_auth(&self, req: RequestBuilder) -> Result<RequestBuilder, NeocitiesError> {
109 if !self.has_auth {
110 return Err(NeocitiesError::AuthError)
111 }
112 Ok(match &self.api_key {
113 Some(k) => req.bearer_auth(k),
114 None => req.basic_auth(&self.username, Some(&self.password))
115 })
116 }
117 fn get(&self, endpoint: &str) -> Result<String, NeocitiesError> {
118 let url = format!("https://neocities.org/api/{}", endpoint);
119 Ok(self.get_auth(self.client.get(url))?.send()?.text()?)
120 }
121
122 /// Gets info about the auth user's site.
123 pub fn info(&self) -> Result<String, NeocitiesError> {
124 self.get("info")
125 }
126 /// Gets info about the given site.
127 ///
128 /// Does not error if the site doesn't exist, but the returned value will be an error message from Neocities.
129 pub fn info_no_auth(&self, site_name: &str) -> Result<String, NeocitiesError> {
130 let url = format!("https://neocities.org/api/info?sitename={}", site_name); // doesn't need auth, just send it raw
131 Ok(self.client.get(&url).send()?.text()?)
132 }
133
134 /// Lists all files and directories on the auth user's site.
135 pub fn list_all(&self) -> Result<String, NeocitiesError> {
136 self.get("list")
137 }
138 /// Lists files and directories starting from the specified path.
139 pub fn list(&self, path: &str) -> Result<String, NeocitiesError> {
140 self.get(&format!("list?path={}", path))
141 }
142
143 /// Uploads a local file to the site, placing it at `remote_path` relative to the site root.
144 ///
145 /// Returns an error if the file can't be opened.
146 ///
147 /// ## Example
148 ///
149 /// ```no_run
150 /// # use rs_neocities::client::NeocitiesClient;
151 /// # use std::fs;
152 /// # let key = String::new();
153 /// let c = NeocitiesClient::new_with_key(&key);
154 /// c.upload("site/index.html", "index.html");
155 /// ```
156 pub fn upload(&self, local_path: impl AsRef<Path>, remote_path: &str) -> Result<String, NeocitiesError> {
157 let v = vec![(local_path, remote_path)];
158 self.upload_multiple(&v) // reduce code reuse
159 }
160 /// Uploads multiple local files to the site. Path tuples should take the form `(local, remote)`,
161 /// where `local` is the local path, and `remote` is the desired remote path relative to the root.
162 ///
163 /// Returns an error if any of the files can't be opened.
164 ///
165 /// ## Example
166 ///
167 /// ```no_run
168 /// # use rs_neocities::client::NeocitiesClient;
169 /// # use std::fs;
170 /// # let key = String::new();
171 /// let c = NeocitiesClient::new_with_key(&key);
172 ///
173 /// let mut files = Vec::new();
174 /// files.push(("site/index.html", "index.html"));
175 /// files.push(("images/favicon.ico", "favicon.ico"));
176 ///
177 /// c.upload_multiple(files);
178 /// ```
179 pub fn upload_multiple(&self, paths: &[(impl AsRef<Path>, &str)]) -> Result<String, NeocitiesError> {
180 let mut files = Vec::new();
181 for (local, remote) in paths {
182 files.push((read(local)?, remote))
183 }
184
185 self.upload_bytes_multiple(files)
186 }
187 /// Uploads a vector of bytes to the site as a file, placing it at `remote_path` relative to the site root.
188 /// This is useful if you're generating data directly from an application,
189 /// and want to upload it without having to save it to a file first.
190 ///
191 /// ## Example
192 ///
193 /// ```no_run
194 /// # use rs_neocities::client::NeocitiesClient;
195 /// # use std::fs;
196 /// # let key = String::new();
197 /// let c = NeocitiesClient::new_with_key(&key);
198 /// let bytes = String::from("hello world!").bytes().collect();
199 /// c.upload_bytes(bytes, "hello.txt");
200 /// ```
201 pub fn upload_bytes(&self, bytes: Vec<u8>, remote_path: &str) -> Result<String, NeocitiesError> {
202 let v = vec![(bytes, remote_path)];
203 self.upload_bytes_multiple(v)
204 }
205 /// Uploads multiple vectors of bytes to the site as files.
206 /// Tuples should take the form `(data, remote)`, where `data` is the data,
207 /// and `remote` is the desired remote path relative to the root.
208 ///
209 /// ## Example
210 ///
211 /// ```no_run
212 /// # use rs_neocities::client::NeocitiesClient;
213 /// # use std::fs;
214 /// # let key = String::new();
215 /// let c = NeocitiesClient::new_with_key(&key);
216 ///
217 /// let data = Vec::new();
218 /// data.push((String::from("hello world!").bytes().collect(), "hello.txt"));
219 /// let generated_data = get_data();
220 /// data.push((generated_data, "data.bin"))
221 ///
222 /// c.upload_bytes_multiple(data);
223 /// ```
224 pub fn upload_bytes_multiple(&self, bytes: Vec<(Vec<u8>, impl AsRef<str>)>) -> Result<String, NeocitiesError> {
225 let mut form = Form::new();
226
227 for (data, path) in bytes {
228 let part = Part::bytes(data).file_name(String::from(path.as_ref()));
229 form = form.part("", part)
230 }
231
232 Ok(self.get_auth(self.client.post("https://neocities.org/api/upload").multipart(form))?.send()?.text()?)
233 }
234
235 /// Delete a file on the site.
236 /// `path` is from the site root.
237 pub fn delete(&self, path: &str) -> Result<String, NeocitiesError> {
238 let v = vec![path];
239 self.delete_multiple(v)
240 }
241
242 /// Delete multiple files.
243 pub fn delete_multiple(&self, files: Vec<&str>) -> Result<String, NeocitiesError> {
244 let mut req = self.get_auth(self.client.post("https://neocities.org/api/delete"))?;
245
246 for f in files {
247 req = req.query(&[("filenames[]", f)]);
248 }
249
250 Ok(req.send()?.text()?)
251 }
252
253 /// Gets the API key for the auth user. You generally only need to get this once,
254 /// so I would recommend just doing it with curl:
255 ///
256 /// ```sh
257 /// curl "https://USER:PASS@neocities.org/api/key"
258 /// ```
259 pub fn get_key(&self) -> Result<String, NeocitiesError> {
260 self.get("key")
261 }
262}
263
264#[derive(Error, Debug)]
265pub enum NeocitiesError {
266 #[error("http request error")]
267 RequestError(#[from] reqwest::Error),
268 #[error("local file read error")]
269 FileError(#[from] std::io::Error),
270 #[error("authentication error")]
271 AuthError,
272 #[error("site item list parse error")]
273 ListParseError
274}