Skip to content

Commit 68d631c

Browse files
authored
Merge pull request #281 from sourcefrog/transport-ref
Transport metadata mtime, and URLs in Transport::Error
2 parents 896ab59 + b54c9db commit 68d631c

File tree

4 files changed

+147
-72
lines changed

4 files changed

+147
-72
lines changed

src/transport.rs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::{error, fmt, io, result};
2020

2121
use bytes::Bytes;
2222
use derive_more::Display;
23+
use time::OffsetDateTime;
2324
use url::Url;
2425

2526
use crate::*;
@@ -45,6 +46,7 @@ pub mod s3;
4546
/// support streaming or partial reads and writes.
4647
#[derive(Clone)]
4748
pub struct Transport {
49+
/// The concrete protocol implementation: local, S3, etc.
4850
protocol: Arc<dyn Protocol + 'static>,
4951
}
5052

@@ -84,7 +86,7 @@ impl Transport {
8486
_other => {
8587
return Err(Error {
8688
kind: ErrorKind::UrlScheme,
87-
path: Some(url.as_str().to_owned()),
89+
url: Some(url.clone()),
8890
source: None,
8991
})
9092
}
@@ -174,9 +176,18 @@ pub enum WriteMode {
174176

175177
trait Protocol: Send + Sync {
176178
fn read_file(&self, path: &str) -> Result<Bytes>;
179+
180+
/// Write a complete file.
181+
///
182+
/// Depending on the [WriteMode] this may either overwrite existing files, or error.
183+
///
184+
/// As much as possible, the file should be written atomically so that it is only visible with
185+
/// the complete content.
177186
fn write_file(&self, relpath: &str, content: &[u8], mode: WriteMode) -> Result<()>;
178187
fn list_dir(&self, relpath: &str) -> Result<ListDir>;
179188
fn create_dir(&self, relpath: &str) -> Result<()>;
189+
190+
/// Get metadata about a file.
180191
fn metadata(&self, relpath: &str) -> Result<Metadata>;
181192

182193
/// Delete a file.
@@ -212,6 +223,9 @@ pub struct Metadata {
212223

213224
/// Kind of file.
214225
pub kind: Kind,
226+
227+
/// Last modified time.
228+
pub modified: OffsetDateTime,
215229
}
216230

217231
/// A list of all the files and directories in a directory.
@@ -225,11 +239,11 @@ pub struct ListDir {
225239
#[derive(Debug)]
226240
pub struct Error {
227241
/// What type of generally known error?
228-
kind: ErrorKind,
242+
pub kind: ErrorKind,
229243
/// The underlying error: for example an IO or S3 error.
230-
source: Option<Box<dyn error::Error + Send + Sync>>,
231-
/// The affected path, possibly relative to the transport.
232-
path: Option<String>,
244+
pub source: Option<Box<dyn error::Error + Send + Sync>>,
245+
/// The affected URL, if known.
246+
pub url: Option<Url>,
233247
}
234248

235249
/// General categories of transport errors.
@@ -244,6 +258,12 @@ pub enum ErrorKind {
244258
#[display(fmt = "Permission denied")]
245259
PermissionDenied,
246260

261+
#[display(fmt = "Create transport error")]
262+
CreateTransport,
263+
264+
#[display(fmt = "Connect error")]
265+
Connect,
266+
247267
#[display(fmt = "Unsupported URL scheme")]
248268
UrlScheme,
249269

@@ -268,28 +288,35 @@ impl Error {
268288
}
269289

270290
pub(self) fn io_error(path: &Path, source: io::Error) -> Error {
291+
let kind = match source.kind() {
292+
io::ErrorKind::NotFound => ErrorKind::NotFound,
293+
io::ErrorKind::AlreadyExists => ErrorKind::AlreadyExists,
294+
io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied,
295+
_ => ErrorKind::Other,
296+
};
297+
271298
Error {
272-
kind: source.kind().into(),
273299
source: Some(Box::new(source)),
274-
path: Some(path.to_string_lossy().to_string()),
300+
url: Url::from_file_path(path).ok(),
301+
kind,
275302
}
276303
}
277304

278305
pub fn is_not_found(&self) -> bool {
279306
self.kind == ErrorKind::NotFound
280307
}
281308

282-
/// The transport-relative path where this error occurred, if known.
283-
pub fn path(&self) -> Option<&str> {
284-
self.path.as_deref()
309+
/// The URL where this error occurred, if known.
310+
pub fn url(&self) -> Option<&Url> {
311+
self.url.as_ref()
285312
}
286313
}
287314

288315
impl fmt::Display for Error {
289316
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290317
write!(f, "{}", self.kind)?;
291-
if let Some(ref path) = self.path {
292-
write!(f, ": {}", path)?;
318+
if let Some(ref url) = self.url {
319+
write!(f, ": {url}")?;
293320
}
294321
if let Some(source) = &self.source {
295322
// I'm not sure we should write this here; it might be repetitive.

src/transport/local.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,14 @@ impl super::Protocol for Protocol {
133133
fn metadata(&self, relpath: &str) -> Result<Metadata> {
134134
let path = self.full_path(relpath);
135135
let fsmeta = path.metadata().map_err(|err| Error::io_error(&path, err))?;
136+
let modified = fsmeta
137+
.modified()
138+
.map_err(|err| Error::io_error(&path, err))?
139+
.into();
136140
Ok(Metadata {
137141
len: fsmeta.len(),
138142
kind: fsmeta.file_type().into(),
143+
modified,
139144
})
140145
}
141146

@@ -164,9 +169,11 @@ impl super::Protocol for Protocol {
164169
#[cfg(test)]
165170
mod test {
166171
use std::error::Error;
172+
use std::time::Duration;
167173

168174
use assert_fs::prelude::*;
169175
use predicates::prelude::*;
176+
use time::OffsetDateTime;
170177

171178
use super::*;
172179
use crate::kind::Kind;
@@ -200,7 +207,12 @@ mod test {
200207
assert!(message.contains("Not found"));
201208
assert!(message.contains("nonexistent.json"));
202209

203-
assert!(err.path().expect("path").ends_with("nonexistent.json"));
210+
assert!(err
211+
.url
212+
.as_ref()
213+
.expect("url")
214+
.path()
215+
.ends_with("/nonexistent.json"));
204216
assert_eq!(err.kind(), transport::ErrorKind::NotFound);
205217
assert!(err.is_not_found());
206218

@@ -218,13 +230,12 @@ mod test {
218230

219231
let transport = Transport::local(temp.path());
220232

221-
assert_eq!(
222-
transport.metadata(filename).unwrap(),
223-
Metadata {
224-
len: 24,
225-
kind: Kind::File
226-
}
227-
);
233+
let metadata = transport.metadata(filename).unwrap();
234+
dbg!(&metadata);
235+
236+
assert_eq!(metadata.len, 24);
237+
assert_eq!(metadata.kind, Kind::File);
238+
assert!(metadata.modified + Duration::from_secs(60) > OffsetDateTime::now_utc());
228239
assert!(transport.metadata("nopoem").unwrap_err().is_not_found());
229240
}
230241

src/transport/s3.rs

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
// cargo mutants -f s3.rs --no-config -C --features=s3,s3-integration-test
2626

2727
use std::fmt;
28-
use std::path::Path;
2928
use std::sync::Arc;
29+
use std::time::SystemTime;
3030

3131
use aws_config::{AppName, BehaviorVersion};
3232
use aws_sdk_s3::error::SdkError;
@@ -73,7 +73,11 @@ impl Protocol {
7373
let runtime = tokio::runtime::Builder::new_current_thread()
7474
.enable_all()
7575
.build()
76-
.map_err(|err| Error::io_error(Path::new(""), err))?;
76+
.map_err(|source| Error {
77+
kind: ErrorKind::CreateTransport,
78+
url: Some(url.to_owned()),
79+
source: Some(Box::new(source)),
80+
})?;
7781

7882
let bucket = url.authority().to_owned();
7983
assert!(!bucket.is_empty(), "S3 bucket name is empty in {url:?}");
@@ -121,6 +125,24 @@ impl Protocol {
121125
fn join_path(&self, relpath: &str) -> String {
122126
join_paths(&self.base_path, relpath)
123127
}
128+
129+
fn s3_error<E, R>(&self, key: &str, source: SdkError<E, R>) -> Error
130+
where
131+
E: std::error::Error + Send + Sync + 'static,
132+
R: std::fmt::Debug + Send + Sync + 'static,
133+
ErrorKind: for<'a> From<&'a E>,
134+
{
135+
debug!(s3_error = ?source);
136+
let kind = match &source {
137+
SdkError::ServiceError(service_err) => ErrorKind::from(service_err.err()),
138+
_ => ErrorKind::Other,
139+
};
140+
Error {
141+
kind,
142+
url: self.url.join(key).ok(),
143+
source: Some(source.into()),
144+
}
145+
}
124146
}
125147

126148
impl fmt::Debug for Protocol {
@@ -221,7 +243,7 @@ impl super::Protocol for Protocol {
221243
result.files.push(name.to_owned());
222244
}
223245
}
224-
Some(Err(err)) => return Err(s3_error(prefix, err)),
246+
Some(Err(err)) => return Err(self.s3_error(&prefix, err)),
225247
None => break,
226248
}
227249
}
@@ -240,13 +262,13 @@ impl super::Protocol for Protocol {
240262
let response = self
241263
.runtime
242264
.block_on(request.send())
243-
.map_err(|source| s3_error(key.clone(), source))?;
265+
.map_err(|source| self.s3_error(&key, source))?;
244266
let body_bytes = self
245267
.runtime
246268
.block_on(response.body.collect())
247269
.map_err(|source| Error {
248270
kind: ErrorKind::Other,
249-
path: Some(key.clone()),
271+
url: self.url.join(relpath).ok(),
250272
source: Some(Box::new(source)),
251273
})?
252274
.into_bytes();
@@ -279,7 +301,7 @@ impl super::Protocol for Protocol {
279301
}
280302
let response = self.runtime.block_on(request.send());
281303
// trace!(?response);
282-
response.map_err(|err| s3_error(key, err))?;
304+
response.map_err(|err| self.s3_error(&key, err))?;
283305
trace!(body_len = content.len(), "wrote file");
284306
Ok(())
285307
}
@@ -290,7 +312,7 @@ impl super::Protocol for Protocol {
290312
let request = self.client.delete_object().bucket(&self.bucket).key(&key);
291313
let response = self.runtime.block_on(request.send());
292314
trace!(?response);
293-
response.map_err(|err| s3_error(key, err))?;
315+
response.map_err(|err| self.s3_error(&key, err))?;
294316
trace!("deleted file");
295317
Ok(())
296318
}
@@ -311,7 +333,7 @@ impl super::Protocol for Protocol {
311333
let mut n_files = 0;
312334
while let Some(response) = self.runtime.block_on(stream.next()) {
313335
for object in response
314-
.map_err(|err| s3_error(prefix.clone(), err))?
336+
.map_err(|err| self.s3_error(&prefix, err))?
315337
.contents
316338
.expect("ListObjectsV2Response has contents")
317339
{
@@ -324,7 +346,7 @@ impl super::Protocol for Protocol {
324346
.key(&key)
325347
.send(),
326348
)
327-
.map_err(|err| s3_error(key, err))?;
349+
.map_err(|err| self.s3_error(&key, err))?;
328350
n_files += 1;
329351
}
330352
}
@@ -346,14 +368,20 @@ impl super::Protocol for Protocol {
346368
.expect("S3 HeadObject response should have a content_length")
347369
.try_into()
348370
.expect("Content length non-negative");
371+
let modified: SystemTime = response
372+
.last_modified
373+
.expect("S3 HeadObject response should have a last_modified")
374+
.try_into()
375+
.expect("S3 last_modified is valid SystemTime");
349376
trace!(?len, "File exists");
350377
Ok(Metadata {
351378
kind: Kind::File,
352379
len,
380+
modified: modified.into(),
353381
})
354382
}
355383
Err(err) => {
356-
let translated = s3_error(key, err);
384+
let translated = self.s3_error(&key, err);
357385
if translated.is_not_found() {
358386
trace!("file does not exist");
359387
} else {
@@ -380,25 +408,6 @@ impl super::Protocol for Protocol {
380408
}
381409
}
382410

383-
fn s3_error<K, E, R>(key: K, source: SdkError<E, R>) -> Error
384-
where
385-
K: ToOwned<Owned = String>,
386-
E: std::error::Error + Send + Sync + 'static,
387-
R: std::fmt::Debug + Send + Sync + 'static,
388-
ErrorKind: for<'a> From<&'a E>,
389-
{
390-
debug!(s3_error = ?source);
391-
let kind = match &source {
392-
SdkError::ServiceError(service_err) => ErrorKind::from(service_err.err()),
393-
_ => ErrorKind::Other,
394-
};
395-
Error {
396-
kind,
397-
path: Some(key.to_owned()),
398-
source: Some(source.into()),
399-
}
400-
}
401-
402411
impl From<&GetObjectError> for ErrorKind {
403412
fn from(source: &GetObjectError) -> Self {
404413
match source {

0 commit comments

Comments
 (0)