1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
use reqwest::{Method, RequestBuilder, Response, StatusCode};
use super::core::{PublicStorage, SessionStorage};
use super::resource::{IntoPubkyResource, IntoResourcePath};
use super::stats::ResourceStats;
use crate::{PubkyHttpClient, Result, cross_log};
/// Interpret the result of a `HEAD` request into a shared outcome used by both
/// session and public storage clients.
async fn interpret_head(client: &PubkyHttpClient, resp: Response) -> Result<Option<Response>> {
match resp.status() {
StatusCode::NOT_FOUND | StatusCode::GONE => {
cross_log!(debug, "HEAD request returned {}", resp.status());
Ok(None)
}
_ => {
cross_log!(debug, "HEAD request returned {}", resp.status());
Ok(Some(client.check_http_status(resp).await?))
}
}
}
/// Send a prepared request and ensure the HTTP status indicates success.
async fn send_checked(client: &PubkyHttpClient, rb: RequestBuilder) -> Result<Response> {
let resp = rb.send().await?;
cross_log!(debug, "Request completed with status {}", resp.status());
client.check_http_status(resp).await
}
/// Send a prepared `HEAD` request and interpret the outcome.
async fn send_head(client: &PubkyHttpClient, rb: RequestBuilder) -> Result<Option<Response>> {
let resp = rb.send().await?;
cross_log!(
debug,
"HEAD request completed with status {}",
resp.status()
);
interpret_head(client, resp).await
}
//
// SessionStorage (authenticated, as-me)
//
impl SessionStorage {
/// HTTP `GET` (as me) for an **absolute path**.
///
/// Returns a successful response; reading its body can still fail.
///
/// # Examples
/// ```no_run
/// # async fn ex(session: pubky::PubkySession) -> pubky::Result<()> {
/// let text = session
/// .storage()
/// .get("/pub/my-cool-app/hello.txt").await?
/// .text().await?;
/// # Ok(()) }
/// ```
///
/// # Errors
/// Missing resources (404/410) return [`crate::errors::RequestError::Server`].
/// See [`SessionStorage`] for shared errors.
pub async fn get<P: IntoResourcePath>(&self, path: P) -> Result<Response> {
let rb = self.request(Method::GET, path).await?;
self.client.check_http_status(self.send(rb).await?).await
}
/// Lightweight existence check (HEAD) for an **absolute path**.
///
/// Returns `Ok(true)` on success or `Ok(false)` on HTTP 404/410.
///
/// # Errors
/// Other failures return `Err`; see [`SessionStorage`].
pub async fn exists<P: IntoResourcePath>(&self, path: P) -> Result<bool> {
let rb = self.request(Method::HEAD, path).await?;
Ok(interpret_head(&self.client, self.send(rb).await?)
.await?
.is_some())
}
/// Retrieve metadata via `HEAD` for an **absolute path** (no body).
///
/// Returns `Ok(Some(stats))` on success or `Ok(None)` on HTTP 404/410.
/// Missing or unparseable headers leave the corresponding [`ResourceStats`]
/// fields `None`.
///
/// # Errors
/// Other failures return `Err`; see [`SessionStorage`].
pub async fn stats<P: IntoResourcePath>(&self, path: P) -> Result<Option<ResourceStats>> {
let rb = self.request(Method::HEAD, path).await?;
Ok(interpret_head(&self.client, self.send(rb).await?)
.await?
.map(|resp| ResourceStats::from_headers(resp.headers())))
}
/// HTTP `PUT` (write) for an **absolute path**.
///
/// Creates or replaces a file and returns the successful response.
/// Requires write permission.
///
/// # Errors
/// Directory targets (400), file/directory conflicts (409), and exceeded quotas
/// (507) return server errors. Body uploads can also fail.
/// A locked path returns 423; the write is not sent again, so retry it if
/// that suits the caller. See [`Self::put_locked`](SessionStorage::put_locked).
/// See [`SessionStorage`] for shared errors.
pub async fn put<P, B>(&self, path: P, body: B) -> Result<Response>
where
P: IntoResourcePath,
B: Into<reqwest::Body>,
{
let rb = self.request(Method::PUT, path).await?.body(body);
self.client.check_http_status(self.send(rb).await?).await
}
/// Delete a file at an **absolute path** and return the successful response.
///
/// Requires write permission.
///
/// # Errors
/// Missing files return 404; directory targets return 400. A locked path
/// returns 423, as for [`Self::put`].
/// See [`SessionStorage`] for shared errors.
pub async fn delete<P: IntoResourcePath>(&self, path: P) -> Result<Response> {
let rb = self.request(Method::DELETE, path).await?;
self.client.check_http_status(self.send(rb).await?).await
}
}
//
// PublicStorage (unauthenticated, any user)
//
impl PublicStorage {
/// HTTP `GET` for an **addressed resource** (`pubky://<pk>/<path>`, `pubky<pk>/<path>`, or `(PublicKey, path)` tuple).
///
/// # Examples
/// ```no_run
/// # async fn ex(user: pubky::PublicKey) -> pubky::Result<()> {
/// let storage = pubky::PublicStorage::new()?;
/// let addr = format!("pubky://{}/pub/my-cool-app/file.txt", user.z32());
/// let resp = storage.get(addr).await?;
/// let bytes = resp.bytes().await?;
///
/// // Or use a tuple:
/// let resp2 = storage.get((&user, "/pub/my-cool-app/file.txt")).await?;
/// # Ok(()) }
/// ```
///
/// # Errors
/// - [`crate::errors::Error::Request`] on HTTP transport failures or when the server
/// responds with a non-success status (the server message is captured).
/// - [`crate::errors::Error::Parse`] if `addr` cannot be converted into a valid
/// addressed resource/URL.
pub async fn get<A: IntoPubkyResource>(&self, addr: A) -> Result<Response> {
let rb = self.request(Method::GET, addr).await?;
send_checked(&self.client, rb).await
}
/// HEAD existence check for an addressed resource.
///
/// # Errors
/// - Propagates transport failures while issuing the `HEAD` request.
/// - Returns [`crate::errors::Error::Parse`] if `addr` cannot be converted into a valid addressed resource.
pub async fn exists<A: IntoPubkyResource>(&self, addr: A) -> Result<bool> {
let rb = self.request(Method::HEAD, addr).await?;
Ok(send_head(&self.client, rb).await?.is_some())
}
/// Metadata via `HEAD` for an addressed resource (no body).
///
/// # Errors
/// - Propagates transport failures while issuing the `HEAD` request.
/// - Returns [`crate::errors::Error::Parse`] if `addr` cannot be converted into a valid addressed resource.
pub async fn stats<A: IntoPubkyResource>(&self, addr: A) -> Result<Option<ResourceStats>> {
let rb = self.request(Method::HEAD, addr).await?;
Ok(send_head(&self.client, rb)
.await?
.map(|resp| ResourceStats::from_headers(resp.headers())))
}
}