Skip to content

Commit

Permalink
feat: add paste expirations
Browse files Browse the repository at this point in the history
Close #7.
  • Loading branch information
Kyle Clemens committed Jul 14, 2018
1 parent 4080149 commit 060ea58
Show file tree
Hide file tree
Showing 43 changed files with 990 additions and 54 deletions.
3 changes: 2 additions & 1 deletion .docker/run/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ done
diesel migration --migration-dir=webserver/migrations run

cargo build -p worker_email "$@"
cargo build -p worker_delete_all_pastes "$@"
cargo build -p worker_delete_directory "$@"
cargo build -p worker_expire_paste "$@"

cargo run "$@" -p webserver config.toml
16 changes: 9 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ script:
# cannot use --all because apparently upgrading sodiumoxide to 0.1 breaks lettre across projects
- cargo build --verbose -p webserver
- cargo build --verbose -p worker_email
- cargo build --verbose -p worker_delete_all_pastes
- cargo build --verbose -p worker_delete_directory
- cargo build --verbose -p worker_expire_paste
# Remove the local crate's build files, as they only add bloat to the cache.
# TODO: Remove unused dependency build files
before_cache:
- rm -rfv target/debug/{webserver,libdelete_all_pastes,libemail}.d
- rm -rfv target/debug/incremental/{build_script_build,webserver,worker_delete_all_pastes,worker_email}-*
- rm -rfv target/debug/.fingerprint/{webserver,worker_delete_all_pastes,worker_email}-*
- rm -rfv target/debug/build/{webserver,worker_delete_all_pastes,worker_email}-*
- rm -rfv target/debug/deps/{webserver,worker_delete_all_pastes,worker_email}-*
- rm -rfv target/debug/{webserver,libworker_delete_directory,libworker_email,libworker_expire_paste}.d
- rm -rfv target/debug/incremental/{build_script_build,webserver,worker_delete_directory,worker_email,worker_expire_paste}-*
- rm -rfv target/debug/.fingerprint/{webserver,worker_delete_directory,worker_email,worker_expire_paste}-*
- rm -rfv target/debug/build/{webserver,worker_delete_directory,worker_email,worker_expire_paste}-*
- rm -rfv target/debug/deps/{webserver,worker_delete_directory,worker_email,worker_expire_paste}-*
# apparently cargo creates this file now? no reason to reupload the whole cache for it
- rm -fv target/.rustc_info.json
- cargo clean -p webserver
- cargo clean -p worker_delete_all_pastes
- cargo clean -p worker_delete_directory
- cargo clean -p worker_email
- cargo clean -p worker_expire_paste
rust:
- nightly-2018-06-28
matrix:
Expand Down
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

members = [
"webserver",
"workers/delete_all_pastes",
"workers/delete_directory",
"workers/email",
"workers/expire_paste",
]
4 changes: 4 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ Create a new paste.
// *unlisted - publicly available to anyone with the link, hidden from crawlers
// private - only visible to the authed user creating the paste
"visibility": "public",
// (optional) the expiration date of the paste
// must be a UTC ISO 8601 string
// pastes do not expire by default
"expires": "2018-07-14T14:07:00Z",
// (required – at least one file) array of files to add to the paste
"files": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alter table pastes drop column expires
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alter table pastes add column expires timestamp
31 changes: 30 additions & 1 deletion webserver/src/backend/pastes/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ use database::models::deletion_keys::NewDeletionKey;
use database::models::pastes::{Paste, NewPaste};
use database::schema::{deletion_keys, pastes};
use models::paste::Visibility;
use sidekiq_::Job;
use store::Store;
use super::models::{PastePayload, CreateSuccess, CreateError};

use chrono::Utc;

use diesel;
use diesel::prelude::*;

use sidekiq::{Client as SidekiqClient, Value};

use unicode_segmentation::UnicodeSegmentation;

use std::borrow::Cow;
Expand All @@ -25,6 +30,12 @@ impl<'a> PastePayload<'a> {
return Err(CreateError::NoFiles);
}

if let Some(expiration_date) = self.expires {
if expiration_date < Utc::now() {
return Err(CreateError::PastExpirationDate);
}
}

if self.files.len() > 1 {
let mut names: Vec<Cow<str>> = self.files.iter()
.enumerate()
Expand Down Expand Up @@ -76,7 +87,7 @@ impl<'a> PastePayload<'a> {
Ok(())
}

pub fn create(self, conn: &DbConn) -> Result<CreateSuccess, CreateError> {
pub fn create(self, conn: &DbConn, sidekiq: &SidekiqClient) -> Result<CreateSuccess, CreateError> {
self.check()?;

let id = Store::new_paste(self.author.map(|x| x.id()))
Expand All @@ -89,6 +100,7 @@ impl<'a> PastePayload<'a> {
self.visibility,
self.author.map(|x| x.id()),
None,
self.expires.map(|x| x.naive_utc()),
);

let paste: Paste = diesel::insert_into(pastes::table)
Expand All @@ -115,6 +127,23 @@ impl<'a> PastePayload<'a> {
files.push(f);
}

if let Some(expiration_date) = self.expires {
let timestamp = expiration_date.timestamp();

let user = match self.author {
Some(a) => a.id().simple().to_string(),
None => "anonymous".to_string(),
};

let job = Job::queue("ExpirePaste", timestamp, vec![
Value::Number(timestamp.into()),
Value::String(Store::directory().to_string_lossy().to_string()),
Value::String(user),
Value::String(id.simple().to_string()),
]);
sidekiq.push(job.into()).map_err(|e| CreateError::Internal(e.into()))?;
}

Ok(CreateSuccess {
paste,
files,
Expand Down
5 changes: 5 additions & 0 deletions webserver/src/backend/pastes/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ use database::models::users::User;
use models::paste::{Content, Visibility};
use utils::Language;

use chrono::{DateTime, Utc};

use failure::Error;

pub struct PastePayload<'u> {
pub name: Option<String>,
pub description: Option<String>,
pub visibility: Visibility,
pub expires: Option<DateTime<Utc>>,
pub author: Option<&'u User>,
pub files: Vec<FilePayload>,
}
Expand Down Expand Up @@ -39,6 +42,7 @@ pub enum CreateError {
FileNameTooLarge,
FileNameTooLong,
EmptyFile,
PastExpirationDate,
Internal(Error),
}

Expand All @@ -56,6 +60,7 @@ impl BackendError for CreateError {
CreateError::FileNameTooLarge => "file name must be less than 25 KiB",
CreateError::FileNameTooLong => "file name must be less than or equal to 255 characters",
CreateError::EmptyFile => "file content must not be empty",
CreateError::PastExpirationDate => "paste expiry date cannot be in the past",
};

Ok(m)
Expand Down
43 changes: 40 additions & 3 deletions webserver/src/database/models/pastes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use models::id::{FileId, PasteId, UserId};
use models::paste::{Content, Visibility};
use models::paste::update::{MetadataUpdate, Update};
use models::status::ErrorKind;
use sidekiq_::Job;
use store::Store;
use super::files::{File as DbFile, NewFile};
use super::super::schema::{pastes, files};
Expand All @@ -19,6 +20,8 @@ use git2::{Signature, Repository, IndexAddOption, Status};

use rocket::http::Status as HttpStatus;

use sidekiq::{Client as SidekiqClient, Value};

use uuid::Uuid;

use std::fs::{self, File};
Expand All @@ -35,6 +38,7 @@ pub struct Paste {
author_id: Option<UserId>,
description: Option<String>,
created_at: NaiveDateTime,
expires: Option<NaiveDateTime>,
}

impl Paste {
Expand Down Expand Up @@ -74,10 +78,19 @@ impl Paste {
DateTime::from_utc(self.created_at, Utc)
}

pub fn update(&mut self, conn: &DbConn, update: &MetadataUpdate) -> Result<()> {
pub fn expires(&self) -> Option<DateTime<Utc>> {
self.expires.map(|x| DateTime::from_utc(x, Utc))
}

pub fn set_expires(&mut self, expires: Option<DateTime<Utc>>) {
self.expires = expires.map(|x| x.naive_utc());
}

pub fn update(&mut self, conn: &DbConn, sidekiq: &SidekiqClient, update: &MetadataUpdate) -> Result<()> {
let changed = !update.name.is_ignore()
|| update.visibility.is_some()
|| !update.description.is_ignore();
|| !update.description.is_ignore()
|| !update.expires.is_ignore();
if !changed {
return Ok(());
}
Expand All @@ -94,6 +107,28 @@ impl Paste {
if let Some(ref update) = update.visibility {
self.set_visibility(*update);
}
match update.expires {
Update::Set(ref s) => {
self.set_expires(Some(s.clone()));

let timestamp = s.timestamp();

let user = match self.author_id() {
Some(a) => a.simple().to_string(),
None => "anonymous".to_string(),
};

let job = Job::queue("ExpirePaste", timestamp, vec![
Value::Number(timestamp.into()),
Value::String(Store::directory().to_string_lossy().to_string()),
Value::String(user),
Value::String(self.id().simple().to_string()),
]);
sidekiq.push(job.into())?;
},
Update::Remove => self.set_expires(None),
_ => {},
}
diesel::update(pastes::table)
.filter(pastes::id.eq(self.id))
.set(&*self)
Expand Down Expand Up @@ -249,6 +284,7 @@ pub struct NewPaste {
author_id: Option<UserId>,
description: Option<String>,
created_at: NaiveDateTime,
expires: Option<NaiveDateTime>,
}

impl NewPaste {
Expand All @@ -259,8 +295,9 @@ impl NewPaste {
visibility: Visibility,
author_id: Option<UserId>,
created_at: Option<NaiveDateTime>,
expires: Option<NaiveDateTime>,
) -> Self {
let created_at = created_at.unwrap_or_else(|| Utc::now().naive_utc());
NewPaste { id, name, visibility, author_id, description, created_at }
NewPaste { id, name, visibility, author_id, description, created_at, expires }
}
}
1 change: 1 addition & 0 deletions webserver/src/database/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ table! {
author_id -> Nullable<Uuid>,
description -> Nullable<Text>,
created_at -> Timestamp,
expires -> Nullable<Timestamp>,
}
}

Expand Down
1 change: 1 addition & 0 deletions webserver/src/models/paste/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub struct Metadata {
pub visibility: Visibility,
#[serde(skip_deserializing)]
pub created_at: Option<DateTime<Utc>>,
pub expires: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize)]
Expand Down
13 changes: 12 additions & 1 deletion webserver/src/models/paste/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ pub struct Output {
}

impl Output {
pub fn new<N, D, F>(paste_id: PasteId, author: Option<OutputAuthor>, name: Option<N>, desc: Option<D>, vis: Visibility, created_at: DateTime<Utc>, deletion_key: Option<DeletionKeyId>, files: F) -> Self
pub fn new<N, D, F>(
paste_id: PasteId,
author: Option<OutputAuthor>,
name: Option<N>,
desc: Option<D>,
vis: Visibility,
created_at: DateTime<Utc>,
expires: Option<DateTime<Utc>>,
deletion_key: Option<DeletionKeyId>,
files: F,
) -> Self
where N: AsRef<str>,
D: AsRef<str>,
F: IntoIterator<Item = OutputFile>,
Expand All @@ -30,6 +40,7 @@ impl Output {
name: name.map(|x| x.as_ref().to_string().into()),
description: desc.map(|x| x.as_ref().to_string().into()),
visibility: vis,
expires,
created_at: Some(created_at),
},
files: Vec::new(),
Expand Down
5 changes: 5 additions & 0 deletions webserver/src/models/paste/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use models::id::FileId;
use models::paste::{Content, CountedText, Visibility};
use utils::Language;

use chrono::{DateTime, Utc};

use serde::de::{Deserialize, Deserializer};

use std::fmt::{self, Debug, Formatter};
Expand Down Expand Up @@ -112,6 +114,9 @@ pub struct MetadataUpdate {
// visibility)
#[serde(default)]
pub visibility: Option<Visibility>,
// double option because expires can be removed, changed, or left alone
#[serde(default)]
pub expires: Update<DateTime<Utc>>,
}

#[derive(Debug, Deserialize)]
Expand Down
2 changes: 2 additions & 0 deletions webserver/src/routes/api/pastes/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ fn _get_all(query: Option<AllQuery>, conn: DbConn) -> RouteResult<Vec<AllPaste>>
name: x.name().map(Into::into),
description: x.description().map(Into::into),
visibility: x.visibility(),
expires: x.expires(),
created_at: Some(x.created_at()),
},
})
Expand Down Expand Up @@ -110,6 +111,7 @@ fn _get(id: PasteId, query: Option<Query>, user: OptionalUser, conn: DbConn) ->
paste.description(),
paste.visibility(),
paste.created_at(),
paste.expires(),
None,
files,
);
Expand Down
7 changes: 5 additions & 2 deletions webserver/src/routes/api/pastes/patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ use models::status::{Status, ErrorKind};
use routes::{RouteResult, RequiredUser};

use rocket::http::Status as HttpStatus;
use rocket::State;

use rocket_contrib::Json;

use sidekiq::Client as SidekiqClient;

type UpdateResult = ::std::result::Result<Json<MetadataUpdate>, ::rocket_contrib::SerdeError>;

#[patch("/<paste_id>", format = "application/json", data = "<info>")]
pub fn patch(paste_id: PasteId, info: UpdateResult, user: RequiredUser, conn: DbConn) -> RouteResult<()> {
pub fn patch(paste_id: PasteId, info: UpdateResult, user: RequiredUser, conn: DbConn, sidekiq: State<SidekiqClient>) -> RouteResult<()> {
// TODO: can this be a request guard?
let info = match info {
Ok(x) => x.into_inner(),
Expand All @@ -31,7 +34,7 @@ pub fn patch(paste_id: PasteId, info: UpdateResult, user: RequiredUser, conn: Db
}

// update paste and database if necessary
paste.update(&conn, &info)?;
paste.update(&conn, &*sidekiq, &info)?;

// return status (204?)
Ok(Status::show_success(HttpStatus::NoContent, ()))
Expand Down

0 comments on commit 060ea58

Please sign in to comment.