Skip to content

Commit

Permalink
Merge pull request #2133 from scpwiki/WJ-1032-file-ops
Browse files Browse the repository at this point in the history
[WJ-1032] Add file_rollback
  • Loading branch information
emmiegit authored Oct 10, 2024
2 parents ff14c09 + 2eea733 commit e65d333
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 29 deletions.
20 changes: 15 additions & 5 deletions deepwell/migrations/20220906103252_deepwell.sql
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,12 @@ CREATE TABLE page (

-- Enum types for page_revision
CREATE TYPE page_revision_type AS ENUM (
-- standard
'regular',
'rollback',
'undo',

-- special
'create',
'delete',
'undelete',
Expand Down Expand Up @@ -293,7 +298,7 @@ CREATE TABLE page_revision (
),

-- Ensure array is not empty for regular revisions
CHECK (revision_type != 'regular' OR changes != '{}'),
CHECK (revision_type NOT IN ('regular', 'rollback', 'undo') OR changes != '{}'),

-- Ensure page creations are always the first revision
CHECK (revision_number != 0 OR revision_type = 'create'),
Expand Down Expand Up @@ -438,10 +443,15 @@ CREATE TABLE blob_pending (

-- Enum types for file_revision
CREATE TYPE file_revision_type AS ENUM (
-- standard
'regular',
'rollback',

-- special
'create',
'update',
'delete',
'undelete'
'undelete',
'move'
);

CREATE TYPE file_revision_change AS ENUM (
Expand Down Expand Up @@ -512,8 +522,8 @@ CREATE TABLE file_revision (
}'
),

-- Ensure array is not empty for update revisions
CHECK (revision_type != 'update' OR changes != '{}'),
-- Ensure array is not empty for regular revisions
CHECK (revision_type NOT IN ('regular', 'rollback') OR changes != '{}'),

-- Ensure page creations are always the first revision
CHECK (revision_number != 0 OR revision_type = 'create'),
Expand Down
1 change: 1 addition & 0 deletions deepwell/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ async fn build_module(app_state: ServerState) -> anyhow::Result<RpcModule<Server
register!("file_delete", file_delete);
register!("file_move", file_move);
register!("file_restore", file_restore);
register!("file_rollback", file_rollback);
register!("file_hard_delete", file_hard_delete);

// File revisions
Expand Down
16 changes: 15 additions & 1 deletion deepwell/src/endpoints/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::services::blob::BlobService;
use crate::services::file::{
CreateFile, CreateFileOutput, DeleteFile, DeleteFileOutput, EditFile, EditFileOutput,
GetFileDetails, GetFileOutput, MoveFile, MoveFileOutput, RestoreFile,
RestoreFileOutput,
RestoreFileOutput, RollbackFile,
};
use crate::services::Result;
use crate::types::{Bytes, FileDetails};
Expand Down Expand Up @@ -115,6 +115,20 @@ pub async fn file_restore(
FileService::restore(ctx, input).await
}

pub async fn file_rollback(
ctx: &ServiceContext<'_>,
params: Params<'static>,
) -> Result<Option<EditFileOutput>> {
let input: RollbackFile = params.parse()?;

info!(
"Rolling back file {:?} in page ID {} in site ID {} to revision number {}",
input.file, input.page_id, input.site_id, input.revision_number,
);

FileService::rollback(ctx, input).await
}

pub async fn file_move(
ctx: &ServiceContext<'_>,
params: Params<'static>,
Expand Down
14 changes: 10 additions & 4 deletions deepwell/src/hash/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ pub fn sha512_hash(data: &[u8]) -> BlobHash {
let mut hasher = Sha512::new();
hasher.update(data);
let result = hasher.finalize();
slice_to_blob_hash(&result)
}

// Copy data into regular Rust array
let mut bytes = [0; 64];
bytes.copy_from_slice(&result);
bytes
/// Convert a slice into a hash array.
///
/// # Panics
/// Panics if the input slice is not the appropriate size.
pub fn slice_to_blob_hash(slice: &[u8]) -> BlobHash {
let mut hash = [0; 64];
hash.copy_from_slice(slice);
hash
}

/// Converts the given SHA-512 hash into a hex array string.
Expand Down
2 changes: 1 addition & 1 deletion deepwell/src/models/blob_pending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct Model {
pub expected_length: i64,
#[sea_orm(column_type = "Text")]
pub s3_path: String,
#[sea_orm(column_type = "VarBinary(StringLen::None)")]
#[sea_orm(column_type = "VarBinary(StringLen::None)", nullable)]
pub s3_hash: Option<Vec<u8>>,
#[sea_orm(column_type = "Text")]
pub presign_url: String,
Expand Down
1 change: 1 addition & 0 deletions deepwell/src/models/page_category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct Model {
pub site_id: i64,
#[sea_orm(column_type = "Text")]
pub slug: String,
#[sea_orm(column_type = "Text", nullable)]
pub layout: Option<String>,
}

Expand Down
12 changes: 10 additions & 2 deletions deepwell/src/models/sea_orm_active_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ pub enum FileRevisionType {
Create,
#[sea_orm(string_value = "delete")]
Delete,
#[sea_orm(string_value = "move")]
Move,
#[sea_orm(string_value = "regular")]
Regular,
#[sea_orm(string_value = "rollback")]
Rollback,
#[sea_orm(string_value = "undelete")]
Undelete,
#[sea_orm(string_value = "update")]
Update,
}
#[derive(
Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize,
Expand Down Expand Up @@ -60,8 +64,12 @@ pub enum PageRevisionType {
Move,
#[sea_orm(string_value = "regular")]
Regular,
#[sea_orm(string_value = "rollback")]
Rollback,
#[sea_orm(string_value = "undelete")]
Undelete,
#[sea_orm(string_value = "undo")]
Undo,
}
#[derive(
Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize,
Expand Down
6 changes: 2 additions & 4 deletions deepwell/src/services/blob/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

use super::prelude::*;
use crate::hash::slice_to_blob_hash;
use crate::models::blob_pending::{
self, Entity as BlobPending, Model as BlobPendingModel,
};
Expand Down Expand Up @@ -371,11 +372,8 @@ impl BlobService {

debug_assert_eq!(expected_length, size);

let mut hash = [0; 64];
hash.copy_from_slice(&hash_vec);

FinalizeBlobUploadOutput {
hash,
hash: slice_to_blob_hash(&hash_vec),
mime,
size,
created: false,
Expand Down
122 changes: 122 additions & 0 deletions deepwell/src/services/file/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

use super::prelude::*;
use crate::hash::slice_to_blob_hash;
use crate::models::file::{self, Entity as File, Model as FileModel};
use crate::models::file_revision::{
self, Entity as FileRevision, Model as FileRevisionModel,
Expand All @@ -28,9 +29,11 @@ use crate::services::blob::{FinalizeBlobUploadOutput, EMPTY_BLOB_HASH, EMPTY_BLO
use crate::services::file_revision::{
CreateFileRevision, CreateFileRevisionBody, CreateFirstFileRevision,
CreateResurrectionFileRevision, CreateTombstoneFileRevision, FileBlob,
GetFileRevision,
};
use crate::services::filter::{FilterClass, FilterType};
use crate::services::{BlobService, FileRevisionService, FilterService};
use sea_orm::ActiveValue;

#[derive(Debug)]
pub struct FileService;
Expand Down Expand Up @@ -130,12 +133,15 @@ impl FileService {
uploaded_blob_id,
} = body;

let mut new_name = ActiveValue::NotSet;

// Verify name change
//
// If the name isn't changing, then we already verified this
// when the file was originally created.
if let Maybe::Set(ref name) = name {
Self::check_conflicts(ctx, page_id, name, "update").await?;
new_name = ActiveValue::Set(name.clone());

if !bypass_filter {
Self::run_filter(ctx, site_id, Some(name)).await?;
Expand Down Expand Up @@ -168,6 +174,7 @@ impl FileService {
// Update file metadata
let model = file::ActiveModel {
file_id: Set(file_id),
name: new_name,
updated_at: Set(Some(now())),
..Default::default()
};
Expand All @@ -182,6 +189,7 @@ impl FileService {
file_id,
user_id,
revision_comments,
revision_type: FileRevisionType::Regular,
body: CreateFileRevisionBody {
name,
licensing,
Expand Down Expand Up @@ -247,6 +255,7 @@ impl FileService {
file_id,
user_id,
revision_comments,
revision_type: FileRevisionType::Move,
body: CreateFileRevisionBody {
page_id: Maybe::Set(destination_page_id),
..Default::default()
Expand Down Expand Up @@ -397,6 +406,119 @@ impl FileService {
})
}

/// Rolls back a file to be the same as it was in a previous revision.
/// It changes the file to have the exact state it had in a previous
/// revision, regardless of any changes since.
pub async fn rollback(
ctx: &ServiceContext<'_>,
RollbackFile {
site_id,
page_id,
file: reference,
last_revision_id,
revision_number,
revision_comments,
user_id,
bypass_filter,
}: RollbackFile<'_>,
) -> Result<Option<EditFileOutput>> {
let txn = ctx.transaction();

// Ensure file exists
let FileModel { file_id, .. } = Self::get(
ctx,
GetFile {
site_id,
page_id,
file: reference,
},
)
.await?;

// Get target revision and latest revision
let get_revision_input = GetFileRevision {
site_id,
page_id,
file_id,
revision_number,
};

let (target_revision, last_revision) = try_join!(
FileRevisionService::get(ctx, get_revision_input),
FileRevisionService::get_latest(ctx, site_id, page_id, file_id),
)?;

// TODO Handle hidden fields, see https://scuttle.atlassian.net/browse/WJ-1285
let _ = target_revision.hidden;

// Check last revision ID
check_last_revision(&last_revision, last_revision_id)?;

// Extract fields from target revision
let FileRevisionModel {
name,
s3_hash,
mime_hint,
size_hint,
licensing,
..
} = target_revision;

let mut new_name = ActiveValue::NotSet;

// Check name change
if last_revision.name != name {
Self::check_conflicts(ctx, page_id, &name, "rollback").await?;
new_name = ActiveValue::Set(name.clone());

if !bypass_filter {
Self::run_filter(ctx, site_id, Some(&name)).await?;
}
}

// Create new revision
//
// Copy the body of the target revision

let blob = FileBlob {
s3_hash: slice_to_blob_hash(&s3_hash),
mime_hint,
size_hint,
// in a rollback, by definition the blob was already uploaded
blob_created: false,
};

let revision_input = CreateFileRevision {
site_id,
page_id,
file_id,
user_id,
revision_comments,
revision_type: FileRevisionType::Rollback,
body: CreateFileRevisionBody {
name: Maybe::Set(name),
blob: Maybe::Set(blob),
licensing: Maybe::Set(licensing),
page_id: Maybe::Unset, // rollbacks should never move files
},
};

// Add new file revision
let revision_output =
FileRevisionService::create(ctx, revision_input, last_revision).await?;

// Update file metadata
let model = file::ActiveModel {
file_id: Set(file_id),
name: new_name,
updated_at: Set(Some(now())),
..Default::default()
};
model.update(txn).await?;

Ok(revision_output)
}

pub async fn get_optional(
ctx: &ServiceContext<'_>,
GetFile {
Expand Down
Loading

0 comments on commit e65d333

Please sign in to comment.