diff --git a/Cargo.lock b/Cargo.lock index e7332080f..e2a8e420a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,6 +399,7 @@ version = "0.6.0" dependencies = [ "arc-swap 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "badge 0.2.0", + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "comrak 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "crates-index-diff 7.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2248,12 +2249,12 @@ name = "postgres-shared" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "fallible-iterator 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", "postgres-protocol 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ac023e7cb..110bd58ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ log = "0.4" regex = "1" structopt = "0.3" crates-index-diff = "7" -time = "0.1" reqwest = "0.9" semver = "0.9" slug = "=0.1.1" @@ -59,6 +58,10 @@ tera = { version = "1.3.0", features = ["builtins"] } arc-swap = "0.4.6" notify = "4.0.15" +# Date and Time utilities +chrono = { version = "0.4.11", features = ["serde"] } +time = "0.1" # TODO: Remove once `iron` is removed + [target.'cfg(not(windows))'.dependencies] libc = "0.2" @@ -70,7 +73,7 @@ path-slash = "0.1.1" [dependencies.postgres] version = "0.15" -features = ["with-time", "with-serde_json"] +features = ["with-chrono", "with-serde_json"] [dev-dependencies] once_cell = "1.2.0" diff --git a/src/db/add_package.rs b/src/db/add_package.rs index b1d949a4b..9e676a11a 100644 --- a/src/db/add_package.rs +++ b/src/db/add_package.rs @@ -85,7 +85,7 @@ pub(crate) fn add_package_into_database( &[ &crate_id, &metadata_pkg.version, - &cratesio_data.release_time, + &cratesio_data.release_time.naive_utc(), &serde_json::to_value(&dependencies)?, &metadata_pkg.package_name(), &cratesio_data.yanked, diff --git a/src/db/file.rs b/src/db/file.rs index d0c46402e..6cd5ac704 100644 --- a/src/db/file.rs +++ b/src/db/file.rs @@ -13,8 +13,8 @@ use std::path::{Path, PathBuf}; pub(crate) use crate::storage::Blob; -pub(crate) fn get_path(conn: &Connection, path: &str) -> Option { - Storage::new(conn).get(path).ok() +pub(crate) fn get_path(conn: &Connection, path: &str) -> Result { + Storage::new(conn).get(path) } /// Store all files in a directory and return [[mimetype, filename]] as Json diff --git a/src/index/api.rs b/src/index/api.rs index f645ea61e..7af4c7434 100644 --- a/src/index/api.rs +++ b/src/index/api.rs @@ -1,14 +1,13 @@ -use std::io::Read; - use crate::{error::Result, utils::MetadataPackage}; - +use chrono::{DateTime, Utc}; use failure::err_msg; use reqwest::{header::ACCEPT, Client}; +use semver::Version; use serde_json::Value; -use time::Timespec; +use std::io::Read; pub(crate) struct RegistryCrateData { - pub(crate) release_time: Timespec, + pub(crate) release_time: DateTime, pub(crate) yanked: bool, pub(crate) downloads: i32, pub(crate) owners: Vec, @@ -36,18 +35,17 @@ impl RegistryCrateData { } /// Get release_time, yanked and downloads from the registry's API -fn get_release_time_yanked_downloads(pkg: &MetadataPackage) -> Result<(time::Timespec, bool, i32)> { +fn get_release_time_yanked_downloads(pkg: &MetadataPackage) -> Result<(DateTime, bool, i32)> { let url = format!("https://crates.io/api/v1/crates/{}/versions", pkg.name); // FIXME: There is probably better way to do this // and so many unwraps... let client = Client::new(); - let mut res = client - .get(&url[..]) - .header(ACCEPT, "application/json") - .send()?; + let mut res = client.get(&url).header(ACCEPT, "application/json").send()?; + let mut body = String::new(); - res.read_to_string(&mut body).unwrap(); - let json: Value = serde_json::from_str(&body[..])?; + res.read_to_string(&mut body)?; + + let json: Value = serde_json::from_str(&body)?; let versions = json .as_object() .and_then(|o| o.get("versions")) @@ -65,15 +63,15 @@ fn get_release_time_yanked_downloads(pkg: &MetadataPackage) -> Result<(time::Tim .and_then(|v| v.as_str()) .ok_or_else(|| err_msg("Not a JSON object"))?; - if semver::Version::parse(version_num).unwrap().to_string() == pkg.version { + if Version::parse(version_num)?.to_string() == pkg.version { let release_time_raw = version .get("created_at") .and_then(|c| c.as_str()) .ok_or_else(|| err_msg("Not a JSON object"))?; + release_time = Some( - time::strptime(release_time_raw, "%Y-%m-%dT%H:%M:%S") - .unwrap() - .to_timespec(), + DateTime::parse_from_str(release_time_raw, "%Y-%m-%dT%H:%M:%S%.f%:z")? + .with_timezone(&Utc), ); yanked = Some( @@ -95,7 +93,7 @@ fn get_release_time_yanked_downloads(pkg: &MetadataPackage) -> Result<(time::Tim } Ok(( - release_time.unwrap_or_else(time::get_time), + release_time.unwrap_or_else(Utc::now), yanked.unwrap_or(false), downloads.unwrap_or(0), )) diff --git a/src/storage/database.rs b/src/storage/database.rs index 07ad4aefd..0f229c637 100644 --- a/src/storage/database.rs +++ b/src/storage/database.rs @@ -1,4 +1,5 @@ use super::Blob; +use chrono::{DateTime, NaiveDateTime, Utc}; use failure::{Error, Fail}; use postgres::{transaction::Transaction, Connection}; @@ -28,11 +29,12 @@ impl<'a> DatabaseBackend<'a> { Ok(Blob { path: row.get("path"), mime: row.get("mime"), - date_updated: row.get("date_updated"), + date_updated: DateTime::from_utc(row.get::<_, NaiveDateTime>("date_updated"), Utc), content: row.get("content"), }) } } + pub(super) fn store_batch(&self, batch: &[Blob], trans: &Transaction) -> Result<(), Error> { for blob in batch { trans.query( @@ -50,13 +52,14 @@ impl<'a> DatabaseBackend<'a> { #[cfg(test)] mod tests { use super::*; - use time::Timespec; + use chrono::{SubsecRound, Utc}; #[test] fn test_path_get() { crate::test::wrapper(|env| { let conn = env.db().conn(); let backend = DatabaseBackend::new(&conn); + let now = Utc::now(); // Add a test file to the database conn.execute( @@ -64,7 +67,7 @@ mod tests { &[ &"dir/foo.txt", &"text/plain", - &Timespec::new(42, 0), + &now.naive_utc(), &"Hello world!".as_bytes(), ], )?; @@ -74,7 +77,7 @@ mod tests { Blob { path: "dir/foo.txt".into(), mime: "text/plain".into(), - date_updated: Timespec::new(42, 0), + date_updated: now.trunc_subsecs(6), content: "Hello world!".bytes().collect(), }, backend.get("dir/foo.txt")? diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 7d10e076f..7b1c20dc0 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -3,10 +3,8 @@ pub(crate) mod s3; pub(crate) use self::database::DatabaseBackend; pub(crate) use self::s3::S3Backend; -use failure::Error; -use time::Timespec; - -use failure::err_msg; +use chrono::{DateTime, Utc}; +use failure::{err_msg, Error}; use postgres::{transaction::Transaction, Connection}; use std::collections::HashMap; use std::ffi::OsStr; @@ -20,7 +18,7 @@ const MAX_CONCURRENT_UPLOADS: usize = 1000; pub(crate) struct Blob { pub(crate) path: String, pub(crate) mime: String, - pub(crate) date_updated: Timespec, + pub(crate) date_updated: DateTime, pub(crate) content: Vec, } @@ -130,7 +128,7 @@ impl<'a> Storage<'a> { mime: mime.to_string(), content, // this field is ignored by the backend - date_updated: Timespec::new(0, 0), + date_updated: Utc::now(), }) }); loop { @@ -281,9 +279,10 @@ mod test { mime: "text/rust".into(), content: "fn main() {}".into(), path: format!("{}.rs", i), - date_updated: Timespec::new(42, 0), + date_updated: Utc::now(), }) .collect(); + test_roundtrip(&uploads); } @@ -297,6 +296,7 @@ mod test { let files = get_file_list(env::current_dir().unwrap().join("Cargo.toml")).unwrap(); assert_eq!(files[0], std::path::Path::new("Cargo.toml")); } + #[test] fn test_mime_types() { check_mime(".gitignore", "text/plain"); diff --git a/src/storage/s3.rs b/src/storage/s3.rs index 6cf3fbf95..e8eb78d00 100644 --- a/src/storage/s3.rs +++ b/src/storage/s3.rs @@ -1,4 +1,5 @@ use super::Blob; +use chrono::{DateTime, NaiveDateTime, Utc}; use failure::Error; use futures::Future; use log::{error, warn}; @@ -7,7 +8,6 @@ use rusoto_credential::DefaultCredentialsProvider; use rusoto_s3::{GetObjectRequest, PutObjectRequest, S3Client, S3}; use std::convert::TryInto; use std::io::Read; -use time::Timespec; use tokio::runtime::Runtime; #[cfg(test)] @@ -100,9 +100,13 @@ impl<'a> S3Backend<'a> { } } -fn parse_timespec(raw: &str) -> Result { - const TIME_FMT: &str = "%a, %d %b %Y %H:%M:%S %Z"; - Ok(time::strptime(raw, TIME_FMT)?.to_timespec()) +fn parse_timespec(mut raw: &str) -> Result, Error> { + raw = raw.trim_end_matches(" GMT"); + + Ok(DateTime::from_utc( + NaiveDateTime::parse_from_str(raw, "%a, %d %b %Y %H:%M:%S")?, + Utc, + )) } pub(crate) fn s3_client() -> Option { @@ -111,6 +115,7 @@ pub(crate) fn s3_client() -> Option { if std::env::var_os("AWS_ACCESS_KEY_ID").is_none() && std::env::var_os("FORCE_S3").is_none() { return None; } + let creds = match DefaultCredentialsProvider::new() { Ok(creds) => creds, Err(err) => { @@ -118,6 +123,7 @@ pub(crate) fn s3_client() -> Option { return None; } }; + Some(S3Client::new_with( rusoto_core::request::HttpClient::new().unwrap(), creds, @@ -135,6 +141,7 @@ pub(crate) fn s3_client() -> Option { pub(crate) mod tests { use super::*; use crate::test::*; + use chrono::TimeZone; use std::slice; #[test] @@ -142,11 +149,11 @@ pub(crate) mod tests { // Test valid conversions assert_eq!( parse_timespec("Thu, 1 Jan 1970 00:00:00 GMT").unwrap(), - Timespec::new(0, 0) + Utc.ymd(1970, 1, 1).and_hms(0, 0, 0), ); assert_eq!( parse_timespec("Mon, 16 Apr 2018 04:33:50 GMT").unwrap(), - Timespec::new(1523853230, 0) + Utc.ymd(2018, 4, 16).and_hms(4, 33, 50), ); // Test invalid conversion @@ -159,7 +166,7 @@ pub(crate) mod tests { let blob = Blob { path: "dir/foo.txt".into(), mime: "text/plain".into(), - date_updated: Timespec::new(42, 0), + date_updated: Utc::now(), content: "Hello world!".into(), }; @@ -189,15 +196,17 @@ pub(crate) mod tests { "parent/child", "h/i/g/h/l/y/_/n/e/s/t/e/d/_/d/i/r/e/c/t/o/r/i/e/s", ]; + let blobs: Vec<_> = names .iter() .map(|&path| Blob { path: path.into(), mime: "text/plain".into(), - date_updated: Timespec::new(42, 0), + date_updated: Utc::now(), content: "Hello world!".into(), }) .collect(); + s3.upload(&blobs).unwrap(); for blob in &blobs { s3.assert_blob(blob, &blob.path); diff --git a/src/test/fakes.rs b/src/test/fakes.rs index 227a9e255..234c422a0 100644 --- a/src/test/fakes.rs +++ b/src/test/fakes.rs @@ -2,6 +2,7 @@ use super::TestDatabase; use crate::docbuilder::BuildResult; use crate::index::api::RegistryCrateData; use crate::utils::{Dependency, MetadataPackage, Target}; +use chrono::{DateTime, Utc}; use failure::Error; #[must_use = "FakeRelease does nothing until you call .create()"] @@ -54,7 +55,7 @@ impl<'a> FakeRelease<'a> { doc_targets: Vec::new(), default_target: None, registry_crate_data: RegistryCrateData { - release_time: time::get_time(), + release_time: Utc::now(), yanked: false, downloads: 0, owners: Vec::new(), @@ -74,7 +75,7 @@ impl<'a> FakeRelease<'a> { self } - pub(crate) fn release_time(mut self, new: time::Timespec) -> Self { + pub(crate) fn release_time(mut self, new: DateTime) -> Self { self.registry_crate_data.release_time = new; self } diff --git a/src/utils/daemon.rs b/src/utils/daemon.rs index 7d9fe0e33..9f8c79617 100644 --- a/src/utils/daemon.rs +++ b/src/utils/daemon.rs @@ -7,6 +7,7 @@ use crate::{ utils::{github_updater, pubsubhubbub, update_release_activity}, DocBuilder, DocBuilderOptions, }; +use chrono::{Timelike, Utc}; use log::{debug, error, info, warn}; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::path::PathBuf; @@ -221,8 +222,9 @@ pub fn start_daemon(background: bool) { .name("release activity updater".to_string()) .spawn(move || loop { thread::sleep(Duration::from_secs(60)); - let now = time::now(); - if now.tm_hour == 23 && now.tm_min == 55 { + let now = Utc::now(); + + if now.hour() == 23 && now.minute() == 55 { info!("Updating release activity"); if let Err(e) = update_release_activity() { error!("Failed to update release activity: {}", e); diff --git a/src/utils/github_updater.rs b/src/utils/github_updater.rs index 4e9a5e53f..cd36eecb3 100644 --- a/src/utils/github_updater.rs +++ b/src/utils/github_updater.rs @@ -1,4 +1,5 @@ use crate::{db::connect_db, error::Result}; +use chrono::{DateTime, Utc}; use failure::err_msg; use log::debug; use regex::Regex; @@ -11,7 +12,7 @@ struct GitHubFields { stars: i64, forks: i64, issues: i64, - last_commit: time::Timespec, + last_commit: DateTime, } /// Updates github fields in crates table @@ -53,7 +54,7 @@ pub fn github_updater() -> Result<()> { &(fields.stars as i32), &(fields.forks as i32), &(fields.issues as i32), - &(fields.last_commit), + &fields.last_commit.naive_utc(), &crate_id, ], ) @@ -121,12 +122,11 @@ fn get_github_fields(path: &str) -> Result { .unwrap_or(0), forks: obj.get("forks_count").and_then(|d| d.as_i64()).unwrap_or(0), issues: obj.get("open_issues").and_then(|d| d.as_i64()).unwrap_or(0), - last_commit: time::strptime( + last_commit: DateTime::parse_from_rfc3339( obj.get("pushed_at").and_then(|d| d.as_str()).unwrap_or(""), - "%Y-%m-%dT%H:%M:%S", ) - .unwrap_or_else(|_| time::now()) - .to_timespec(), + .map(|datetime| datetime.with_timezone(&Utc)) + .unwrap_or_else(|_| Utc::now()), }) } @@ -152,7 +152,7 @@ fn get_github_path(url: &str) -> Option { #[cfg(test)] mod test { - use super::{get_github_fields, get_github_path, github_updater}; + use super::*; #[test] fn test_get_github_path() { @@ -190,8 +190,7 @@ mod test { assert!(fields.forks >= 0); assert!(fields.issues >= 0); - use time; - assert!(fields.last_commit <= time::now().to_timespec()); + assert!(fields.last_commit <= Utc::now()); } #[test] diff --git a/src/utils/release_activity_updater.rs b/src/utils/release_activity_updater.rs index 35bf6e9fd..ffe92f40a 100644 --- a/src/utils/release_activity_updater.rs +++ b/src/utils/release_activity_updater.rs @@ -1,7 +1,7 @@ use crate::db::connect_db; use crate::error::Result; +use chrono::{Duration, Utc}; use serde_json::{Map, Value}; -use time::{now, Duration}; pub fn update_release_activity() -> Result<()> { let conn = connect_db()?; @@ -37,11 +37,10 @@ pub fn update_release_activity() -> Result<()> { let release_count: i64 = rows.get(0).get(0); let failure_count: i64 = failures_count_rows.get(0).get(0); - let now = now(); + let now = Utc::now().naive_utc(); let date = now - Duration::days(day); - // unwrap is fine here, as our date format is always valid - dates.push(format!("{}", date.strftime("%d %b").unwrap())); + dates.push(format!("{}", date.format("%d %b"))); crate_counts.push(release_count); failure_counts.push(failure_count); } diff --git a/src/web/builds.rs b/src/web/builds.rs index 9c3223003..f318470e2 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -3,6 +3,7 @@ use super::page::Page; use super::pool::Pool; use super::MetaData; use crate::docbuilder::Limits; +use chrono::{DateTime, NaiveDateTime, Utc}; use iron::prelude::*; use router::Router; use serde::ser::{Serialize, SerializeStruct, Serializer}; @@ -13,7 +14,7 @@ struct Build { rustc_version: String, cratesfyi_version: String, build_status: bool, - build_time: time::Timespec, + build_time: DateTime, output: Option, } @@ -27,10 +28,7 @@ impl Serialize for Build { state.serialize_field("rustc_version", &self.rustc_version)?; state.serialize_field("cratesfyi_version", &self.cratesfyi_version)?; state.serialize_field("build_status", &self.build_status)?; - state.serialize_field( - "build_time", - &time::at(self.build_time).rfc3339().to_string(), - )?; + state.serialize_field("build_time", &self.build_time.format("%+").to_string())?; // RFC 3339 state.serialize_field("build_time_relative", &duration_to_str(self.build_time))?; state.serialize_field("output", &self.output)?; @@ -102,7 +100,7 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { rustc_version: row.get(6), cratesfyi_version: row.get(7), build_status: row.get(8), - build_time: row.get(9), + build_time: DateTime::from_utc(row.get::<_, NaiveDateTime>(9), Utc), output: row.get(10), }; @@ -153,11 +151,12 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { #[cfg(test)] mod tests { use super::*; + use chrono::Utc; use serde_json::json; #[test] fn serialize_build() { - let time = time::get_time(); + let time = Utc::now(); let mut build = Build { id: 22, rustc_version: "rustc 1.43.0 (4fb7144ed 2020-04-20)".to_string(), @@ -171,7 +170,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": null, "build_status": true @@ -184,7 +183,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": "some random stuff", "build_status": true @@ -195,7 +194,7 @@ mod tests { #[test] fn serialize_build_page() { - let time = time::get_time(); + let time = Utc::now(); let build = Build { id: 22, rustc_version: "rustc 1.43.0 (4fb7144ed 2020-04-20)".to_string(), @@ -232,7 +231,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": null, "build_status": true @@ -241,7 +240,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": null, "build_status": true @@ -258,7 +257,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": null, "build_status": true @@ -267,7 +266,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": null, "build_status": true @@ -285,7 +284,7 @@ mod tests { "id": 22, "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", - "build_time": time::at(time).rfc3339().to_string(), + "build_time": time.format("%+").to_string(), "build_time_relative": duration_to_str(time), "output": null, "build_status": true diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index 7911ce032..273bc4d96 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -4,6 +4,7 @@ use super::pool::Pool; use super::{ duration_to_str, match_version, redirect_base, render_markdown, MatchSemver, MetaData, }; +use chrono::{DateTime, NaiveDateTime, Utc}; use iron::prelude::*; use iron::{status, Url}; use postgres::Connection; @@ -27,7 +28,7 @@ pub struct CrateDetails { dependencies: Option, readme: Option, rustdoc: Option, // this is description_long in database - release_time: time::Timespec, + release_time: DateTime, build_status: bool, last_successful_build: Option, rustdoc_status: bool, @@ -206,7 +207,7 @@ impl CrateDetails { dependencies: krate.get("dependencies"), readme: krate.get("readme"), rustdoc: krate.get("description_long"), - release_time: krate.get("release_time"), + release_time: DateTime::from_utc(krate.get::<_, NaiveDateTime>("release_time"), Utc), build_status: krate.get("build_status"), last_successful_build: None, rustdoc_status: krate.get("rustdoc_status"), @@ -287,7 +288,7 @@ impl CrateDetails { } #[cfg(test)] - pub fn default_tester(release_time: time::Timespec) -> Self { + pub fn default_tester(release_time: DateTime) -> Self { Self { name: "rcc".to_string(), version: "100.0.0".to_string(), @@ -385,6 +386,7 @@ pub fn crate_details_handler(req: &mut Request) -> IronResult { mod tests { use super::*; use crate::test::TestDatabase; + use chrono::Utc; use failure::Error; use serde_json::json; @@ -634,7 +636,7 @@ mod tests { #[test] fn serialize_crate_details() { - let time = time::get_time(); + let time = Utc::now(); let mut details = CrateDetails::default_tester(time); let mut correct_json = json!({ diff --git a/src/web/file.rs b/src/web/file.rs index bcf7eca1f..7ea0457fe 100644 --- a/src/web/file.rs +++ b/src/web/file.rs @@ -1,17 +1,16 @@ //! Database based file handler use super::pool::Pool; -use crate::db; -use iron::status; -use iron::{Handler, IronError, IronResult, Request, Response}; +use crate::{db, error::Result}; +use iron::{status, Handler, IronError, IronResult, Request, Response}; use postgres::Connection; pub(crate) struct File(pub(crate) db::file::Blob); impl File { /// Gets file from database - pub fn from_path(conn: &Connection, path: &str) -> Option { - Some(File(db::file::get_path(conn, path)?)) + pub fn from_path(conn: &Connection, path: &str) -> Result { + Ok(File(db::file::get_path(conn, path)?)) } /// Consumes File and creates a iron response @@ -27,9 +26,14 @@ impl File { .headers .set(ContentType(self.0.mime.parse().unwrap())); response.headers.set(CacheControl(cache)); - response - .headers - .set(LastModified(HttpDate(time::at(self.0.date_updated)))); + // FIXME: This is so horrible + response.headers.set(LastModified(HttpDate( + time::strptime( + &self.0.date_updated.format("%a, %d %b %Y %T %Z").to_string(), + "%a, %d %b %Y %T %Z", + ) + .unwrap(), + ))); response } @@ -48,7 +52,7 @@ impl Handler for DatabaseFileHandler { fn handle(&self, req: &mut Request) -> IronResult { let path = req.url.path().join("/"); let conn = extension!(req, Pool).get()?; - if let Some(file) = File::from_path(&conn, &path) { + if let Ok(file) = File::from_path(&conn, &path) { Ok(file.serve()) } else { Err(IronError::new( @@ -58,3 +62,35 @@ impl Handler for DatabaseFileHandler { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::wrapper; + use chrono::Utc; + + #[test] + fn file_roundtrip() { + wrapper(|env| { + let db = env.db(); + let now = Utc::now(); + + db.fake_release().create()?; + + let mut file = File::from_path( + &*db.conn(), + "rustdoc/fake-package/1.0.0/fake-package/index.html", + ) + .unwrap(); + file.0.date_updated = now; + + let resp = file.serve(); + assert_eq!( + resp.headers.get_raw("Last-Modified").unwrap(), + [now.format("%a, %d %b %Y %T GMT").to_string().into_bytes()].as_ref(), + ); + + Ok(()) + }); + } +} diff --git a/src/web/mod.rs b/src/web/mod.rs index e208fd329..429c3fe84 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -56,6 +56,7 @@ mod sitemap; mod source; use self::pool::Pool; +use chrono::{DateTime, Utc}; use handlebars_iron::{DirectorySource, HandlebarsEngine, SourceError}; use iron::headers::{CacheControl, CacheDirective, ContentType, Expires, HttpDate}; use iron::modifiers::Redirect; @@ -414,9 +415,10 @@ impl Server { } /// Converts Timespec to nice readable relative time string -fn duration_to_str(ts: time::Timespec) -> String { - let tm = time::at(ts); - let delta = time::now() - tm; +fn duration_to_str(init: DateTime) -> String { + let now = Utc::now(); + let delta = now.signed_duration_since(init); + let delta = ( delta.num_days(), delta.num_hours(), @@ -425,7 +427,7 @@ fn duration_to_str(ts: time::Timespec) -> String { ); match delta { - (days, ..) if days > 5 => format!("{}", tm.strftime("%b %d, %Y").unwrap()), + (days, ..) if days > 5 => format!("{}", init.format("%b %d, %Y")), (days @ 2..=5, ..) => format!("{} days ago", days), (1, ..) => "one day ago".to_string(), diff --git a/src/web/page/handlebars.rs b/src/web/page/handlebars.rs index 59b36e024..05a10c413 100644 --- a/src/web/page/handlebars.rs +++ b/src/web/page/handlebars.rs @@ -152,12 +152,13 @@ fn build_version_safe(version: &str) -> String { mod tests { use super::*; use crate::web::releases::{self, Release}; + use chrono::Utc; use iron::Url; use serde_json::json; #[test] fn serialize_page() { - let time = time::get_time(); + let time = Utc::now(); let mut release = Release::default(); release.name = "lasso".into(); @@ -189,7 +190,7 @@ mod tests { "target_name": null, "rustdoc_status": false, "release_time": super::super::super::duration_to_str(time), - "release_time_rfc3339": time::at(time).rfc3339().to_string(), + "release_time_rfc3339": time.format("%+").to_string(), "stars": 0 }], "varss": { "test": "works" }, diff --git a/src/web/page/templates.rs b/src/web/page/templates.rs index bb38c9333..f7efbc7ac 100644 --- a/src/web/page/templates.rs +++ b/src/web/page/templates.rs @@ -1,6 +1,7 @@ use super::TEMPLATE_DATA; use crate::error::Result; use arc_swap::ArcSwap; +use chrono::{DateTime, Utc}; use serde_json::Value; use std::collections::HashMap; use tera::{Result as TeraResult, Tera}; @@ -142,9 +143,11 @@ fn rustc_resource_suffix(args: &HashMap) -> TeraResult { // TODO: This can be replaced by chrono fn timeformat(value: &Value, args: &HashMap) -> TeraResult { let fmt = if let Some(Value::Bool(true)) = args.get("relative") { - let value = time::strptime(value.as_str().unwrap(), "%Y-%m-%dT%H:%M:%S%z").unwrap(); + let value = DateTime::parse_from_str(value.as_str().unwrap(), "%Y-%m-%dT%H:%M:%S%z") + .unwrap() + .with_timezone(&Utc); - super::super::duration_to_str(value.to_timespec()) + super::super::duration_to_str(value) } else { const TIMES: &[&str] = &["seconds", "minutes", "hours"]; diff --git a/src/web/releases.rs b/src/web/releases.rs index 72b142eda..777c1e770 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -4,6 +4,7 @@ use super::error::Nope; use super::page::Page; use super::pool::Pool; use super::{duration_to_str, match_version, redirect_base}; +use chrono::{DateTime, NaiveDateTime, Utc}; use iron::prelude::*; use iron::status; use postgres::Connection; @@ -25,7 +26,7 @@ pub struct Release { description: Option, target_name: Option, rustdoc_status: bool, - pub(crate) release_time: time::Timespec, + pub(crate) release_time: DateTime, stars: i32, } @@ -37,7 +38,7 @@ impl Default for Release { description: None, target_name: None, rustdoc_status: false, - release_time: time::get_time(), + release_time: Utc::now(), stars: 0, } } @@ -57,7 +58,7 @@ impl Serialize for Release { state.serialize_field("release_time", &duration_to_str(self.release_time))?; state.serialize_field( "release_time_rfc3339", - &time::at(self.release_time).rfc3339().to_string(), + &self.release_time.format("%+").to_string(), )?; state.serialize_field("stars", &self.stars)?; @@ -149,7 +150,7 @@ pub(crate) fn get_releases(conn: &Connection, page: i64, limit: i64, order: Orde version: row.get(1), description: row.get(2), target_name: row.get(3), - release_time: row.get(4), + release_time: DateTime::from_utc(row.get::<_, NaiveDateTime>(4), Utc), rustdoc_status: row.get(5), stars: row.get(6), }) @@ -195,7 +196,7 @@ fn get_releases_by_author( version: row.get(1), description: row.get(2), target_name: row.get(3), - release_time: row.get(4), + release_time: DateTime::from_utc(row.get::<_, NaiveDateTime>(4), Utc), rustdoc_status: row.get(5), stars: row.get(6), } @@ -248,7 +249,7 @@ fn get_releases_by_owner( version: row.get(1), description: row.get(2), target_name: row.get(3), - release_time: row.get(4), + release_time: DateTime::from_utc(row.get::<_, NaiveDateTime>(4), Utc), rustdoc_status: row.get(5), stars: row.get(6), } @@ -332,7 +333,7 @@ fn get_search_results( version: row.get("version"), description: row.get("description"), target_name: row.get("target_name"), - release_time: row.get("release_time"), + release_time: DateTime::from_utc(row.get("release_time"), Utc), rustdoc_status: row.get("rustdoc_status"), stars: row.get::<_, i32>("github_stars"), }) @@ -681,6 +682,7 @@ pub fn build_queue_handler(req: &mut Request) -> IronResult { mod tests { use super::*; use crate::test::{assert_success, wrapper}; + use chrono::TimeZone; use serde_json::json; #[test] @@ -892,25 +894,25 @@ mod tests { let db = env.db(); db.fake_release() .name("somethang") - .release_time(time::Timespec::new(1000, 0)) + .release_time(Utc.ymd(2021, 4, 16).and_hms(4, 33, 50)) .version("0.3.0") .description("this is the correct choice") .create()?; db.fake_release() .name("somethang") - .release_time(time::Timespec::new(100, 0)) + .release_time(Utc.ymd(2020, 4, 16).and_hms(4, 33, 50)) .description("second") .version("0.2.0") .create()?; db.fake_release() .name("somethang") - .release_time(time::Timespec::new(10, 0)) + .release_time(Utc.ymd(2019, 4, 16).and_hms(4, 33, 50)) .description("third") .version("0.1.0") .create()?; db.fake_release() .name("somethang") - .release_time(time::Timespec::new(1, 0)) + .release_time(Utc.ymd(2018, 4, 16).and_hms(4, 33, 50)) .description("fourth") .version("0.0.0") .create()?; @@ -1016,8 +1018,7 @@ mod tests { #[test] fn serialize_releases() { - let current_time = time::get_time(); - let now = time::at(current_time); + let now = Utc::now(); let mut release = Release { name: "serde".to_string(), @@ -1025,7 +1026,7 @@ mod tests { description: Some("serde makes things other things".to_string()), target_name: Some("x86_64-pc-windows-msvc".to_string()), rustdoc_status: true, - release_time: current_time, + release_time: now, stars: 100, }; @@ -1035,8 +1036,8 @@ mod tests { "description": "serde makes things other things", "target_name": "x86_64-pc-windows-msvc", "rustdoc_status": true, - "release_time": duration_to_str(current_time), - "release_time_rfc3339": now.rfc3339().to_string(), + "release_time": duration_to_str(now), + "release_time_rfc3339": now.format("%+").to_string(), "stars": 100 }); @@ -1049,8 +1050,8 @@ mod tests { "description": "serde makes things other things", "target_name": null, "rustdoc_status": true, - "release_time": duration_to_str(current_time), - "release_time_rfc3339": now.rfc3339().to_string(), + "release_time": duration_to_str(now), + "release_time_rfc3339": now.format("%+").to_string(), "stars": 100 }); @@ -1063,8 +1064,8 @@ mod tests { "description": null, "target_name": null, "rustdoc_status": true, - "release_time": duration_to_str(current_time), - "release_time_rfc3339": now.rfc3339().to_string(), + "release_time": duration_to_str(now), + "release_time_rfc3339": now.format("%+").to_string(), "stars": 100 }); diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index 893992529..4d203aa2e 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -138,8 +138,8 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { let path = path.join("/"); let conn = extension!(req, Pool).get()?; match File::from_path(&conn, &path) { - Some(f) => return Ok(f.serve()), - None => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)), + Ok(f) => return Ok(f.serve()), + Err(..) => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)), } } } else if req @@ -294,7 +294,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { } // Attempt to load the file from the database - let file = if let Some(file) = File::from_path(&conn, &path) { + let file = if let Ok(file) = File::from_path(&conn, &path) { file } else { // If it fails, we try again with /index.html at the end @@ -302,7 +302,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { req_path.push("index.html"); File::from_path(&conn, &path) - .ok_or_else(|| IronError::new(Nope::ResourceNotFound, status::NotFound))? + .map_err(|_| IronError::new(Nope::ResourceNotFound, status::NotFound))? }; // Serve non-html files directly @@ -398,7 +398,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { /// Returns a path that can be appended to `/crate/version/` to create a complete URL. fn path_for_version(req_path: &[&str], known_platforms: &[String], conn: &Connection) -> String { // Simple case: page exists in the latest version, so just change the version number - if File::from_path(&conn, &req_path.join("/")).is_some() { + if File::from_path(&conn, &req_path.join("/")).is_ok() { // NOTE: this adds 'index.html' if it wasn't there before return req_path[3..].join("/"); } @@ -552,7 +552,7 @@ impl Handler for SharedResourceHandler { if ["js", "css", "woff", "svg"].contains(&suffix) { let conn = extension!(req, Pool).get()?; - if let Some(file) = File::from_path(&conn, filename) { + if let Ok(file) = File::from_path(&conn, filename) { return Ok(file.serve()); } } @@ -566,6 +566,7 @@ impl Handler for SharedResourceHandler { mod test { use super::*; use crate::test::*; + use chrono::Utc; use reqwest::StatusCode; use serde_json::json; use std::{collections::BTreeMap, iter::FromIterator}; @@ -1372,7 +1373,8 @@ mod test { #[test] fn serialize_rustdoc_page() { - let time = time::get_time(); + let time = Utc::now(); + let details = json!({ "name": "rcc", "version": "100.0.0", diff --git a/src/web/sitemap.rs b/src/web/sitemap.rs index 2fb3eebed..576b89a3b 100644 --- a/src/web/sitemap.rs +++ b/src/web/sitemap.rs @@ -1,5 +1,6 @@ use super::page::Page; use super::pool::Pool; +use chrono::{DateTime, NaiveDateTime, Utc}; use iron::headers::ContentType; use iron::prelude::*; use serde_json::Value; @@ -22,7 +23,9 @@ pub fn sitemap_handler(req: &mut Request) -> IronResult { let releases = query .into_iter() .map(|row| { - let time = format!("{}", time::at(row.get(1)).rfc3339()); + let time = DateTime::::from_utc(row.get::<_, NaiveDateTime>(1), Utc) + .format("%+") + .to_string(); (row.get(0), time) }) diff --git a/src/web/source.rs b/src/web/source.rs index 4851be467..5e6245f4e 100644 --- a/src/web/source.rs +++ b/src/web/source.rs @@ -215,7 +215,7 @@ pub fn source_browser_handler(req: &mut Request) -> IronResult { // try to get actual file first // skip if request is a directory let file = if !file_path.ends_with('/') { - DbFile::from_path(&conn, &file_path) + DbFile::from_path(&conn, &file_path).ok() } else { None };