refactor
This commit is contained in:
@@ -10,7 +10,7 @@ use uuid::Uuid;
|
|||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::models::{Document, DocumentAsset, DocumentAssetObject, DocumentVersion};
|
use crate::models::{Document, DocumentAsset, DocumentAssetObject, DocumentVersion};
|
||||||
use crate::schema::{document_asset_objects, document_assets, document_versions};
|
use crate::schema::{document_asset_objects, document_assets, document_versions};
|
||||||
use crate::state::AppState;
|
use crate::state::{AppState, PgPooledConnection};
|
||||||
use crate::utils::time::to_iso;
|
use crate::utils::time::to_iso;
|
||||||
|
|
||||||
#[derive(Serialize, Clone, ToSchema)]
|
#[derive(Serialize, Clone, ToSchema)]
|
||||||
@@ -158,6 +158,14 @@ pub async fn load_asset_responses(
|
|||||||
version_id: Uuid,
|
version_id: Uuid,
|
||||||
) -> AppResult<Vec<DocumentAssetResponse>> {
|
) -> AppResult<Vec<DocumentAssetResponse>> {
|
||||||
let mut conn = state.db_for_tenant(tenant_id)?;
|
let mut conn = state.db_for_tenant(tenant_id)?;
|
||||||
|
load_asset_responses_with_conn(&mut conn, tenant_id, version_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_asset_responses_with_conn(
|
||||||
|
conn: &mut PgPooledConnection,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
version_id: Uuid,
|
||||||
|
) -> AppResult<Vec<DocumentAssetResponse>> {
|
||||||
let assets: Vec<(DocumentAsset, Option<DocumentAssetObject>)> = document_assets::table
|
let assets: Vec<(DocumentAsset, Option<DocumentAssetObject>)> = document_assets::table
|
||||||
.left_outer_join(
|
.left_outer_join(
|
||||||
document_asset_objects::table.on(document_asset_objects::asset_id
|
document_asset_objects::table.on(document_asset_objects::asset_id
|
||||||
@@ -171,8 +179,7 @@ pub async fn load_asset_responses(
|
|||||||
document_assets::all_columns,
|
document_assets::all_columns,
|
||||||
document_asset_objects::all_columns.nullable(),
|
document_asset_objects::all_columns.nullable(),
|
||||||
))
|
))
|
||||||
.load(&mut conn)?;
|
.load(conn)?;
|
||||||
drop(conn);
|
|
||||||
|
|
||||||
Ok(assets
|
Ok(assets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -186,11 +186,12 @@ pub async fn get_document(
|
|||||||
)]
|
)]
|
||||||
pub async fn upload_document(
|
pub async fn upload_document(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
TenantScopedConn {
|
scoped_conn: TenantScopedConn,
|
||||||
tenant_id, user_id, ..
|
|
||||||
}: TenantScopedConn,
|
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
) -> AppResult<JsonResponse<DocumentDetailResponse>> {
|
) -> AppResult<JsonResponse<DocumentDetailResponse>> {
|
||||||
|
let tenant_id = scoped_conn.tenant_id;
|
||||||
|
let user_id = scoped_conn.user_id;
|
||||||
|
drop(scoped_conn);
|
||||||
let mut file_bytes: Option<Vec<u8>> = None;
|
let mut file_bytes: Option<Vec<u8>> = None;
|
||||||
let mut original_name: Option<String> = None;
|
let mut original_name: Option<String> = None;
|
||||||
let mut content_type: Option<String> = None;
|
let mut content_type: Option<String> = None;
|
||||||
|
|||||||
+197
-175
@@ -20,9 +20,10 @@ use uuid::Uuid;
|
|||||||
use crate::documents::{
|
use crate::documents::{
|
||||||
asset::{
|
asset::{
|
||||||
build_download_path, derive_document_title, filename_with_retained_extension,
|
build_download_path, derive_document_title, filename_with_retained_extension,
|
||||||
load_asset_responses, load_primary_assets, to_asset_detail_response,
|
load_asset_responses, load_asset_responses_with_conn, load_primary_assets,
|
||||||
to_asset_object_response, to_version_response, DocumentAssetDetailResponse,
|
to_asset_detail_response, to_asset_object_response, to_version_response,
|
||||||
DocumentAssetResponse, DocumentVersionDetailResponse, DocumentVersionResponse,
|
DocumentAssetDetailResponse, DocumentAssetResponse, DocumentVersionDetailResponse,
|
||||||
|
DocumentVersionResponse,
|
||||||
},
|
},
|
||||||
correspondents::{
|
correspondents::{
|
||||||
insert_document_correspondents, normalize_correspondent_ids, DocumentCorrespondentResponse,
|
insert_document_correspondents, normalize_correspondent_ids, DocumentCorrespondentResponse,
|
||||||
@@ -660,9 +661,10 @@ impl<'a> DocumentsService<'a> {
|
|||||||
skip_if_existing,
|
skip_if_existing,
|
||||||
} = request;
|
} = request;
|
||||||
|
|
||||||
|
let mut tenant_conn = self.state.db_for_tenant(tenant_id)?;
|
||||||
|
|
||||||
if let Some(folder) = folder_id {
|
if let Some(folder) = folder_id {
|
||||||
let mut conn = self.state.db_for_tenant(tenant_id)?;
|
ensure_folder_exists_on_conn(&mut tenant_conn, tenant_id, folder)?;
|
||||||
ensure_folder_exists_on_conn(&mut conn, tenant_id, folder)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc_id = Uuid::new_v4();
|
let doc_id = Uuid::new_v4();
|
||||||
@@ -681,106 +683,20 @@ impl<'a> DocumentsService<'a> {
|
|||||||
let size_bytes = bytes.len() as i64;
|
let size_bytes = bytes.len() as i64;
|
||||||
let s3_key = document_version_object_key(doc_id, version_number, version_id);
|
let s3_key = document_version_object_key(doc_id, version_number, version_id);
|
||||||
|
|
||||||
|
if let Some(reused) = self
|
||||||
|
.try_reuse_existing_document(
|
||||||
|
&mut tenant_conn,
|
||||||
|
tenant_id,
|
||||||
|
user_id,
|
||||||
|
&checksum_hex,
|
||||||
|
issued_at_override,
|
||||||
|
skip_if_existing,
|
||||||
|
&tag_ids,
|
||||||
|
&correspondents,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
{
|
{
|
||||||
let mut conn = self.state.db_for_tenant(tenant_id)?;
|
return Ok(DocumentUploadOutcome::Reused(reused));
|
||||||
|
|
||||||
let existing = documents::table
|
|
||||||
.inner_join(
|
|
||||||
document_versions::table
|
|
||||||
.on(document_versions::id.eq(documents::current_version_id)),
|
|
||||||
)
|
|
||||||
.filter(documents::tenant_id.eq(tenant_id))
|
|
||||||
.filter(document_versions::checksum.eq(&checksum_hex))
|
|
||||||
.select((documents::all_columns, document_versions::all_columns))
|
|
||||||
.first::<(Document, DocumentVersion)>(&mut conn)
|
|
||||||
.optional()?;
|
|
||||||
|
|
||||||
if let Some((mut document, version)) = existing {
|
|
||||||
if skip_if_existing {
|
|
||||||
info!(
|
|
||||||
document_id = %document.id,
|
|
||||||
checksum = %checksum_hex,
|
|
||||||
"upload rejected because document already exists",
|
|
||||||
);
|
|
||||||
return Err(AppError::conflict(
|
|
||||||
"a document with the same contents already exists",
|
|
||||||
)
|
|
||||||
.with_code("duplicate_document")
|
|
||||||
.with_details(json!({
|
|
||||||
"conflict_document_id": document.id,
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(issued_at) = issued_at_override {
|
|
||||||
if document.issued_at != Some(issued_at) {
|
|
||||||
diesel::update(
|
|
||||||
documents::table
|
|
||||||
.find(document.id)
|
|
||||||
.filter(documents::tenant_id.eq(tenant_id)),
|
|
||||||
)
|
|
||||||
.set((
|
|
||||||
documents::issued_at.eq(Some(issued_at)),
|
|
||||||
documents::updated_at.eq(Utc::now().naive_utc()),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)?;
|
|
||||||
document.issued_at = Some(issued_at);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assign_tags_to_document(&mut conn, tenant_id, &document, &tag_ids, Some(user_id))?;
|
|
||||||
|
|
||||||
if !correspondents.is_empty() {
|
|
||||||
let raw_ids: Vec<Uuid> = correspondents
|
|
||||||
.iter()
|
|
||||||
.map(|assignment| assignment.correspondent_id)
|
|
||||||
.collect();
|
|
||||||
let correspondent_ids = normalize_correspondent_ids(&raw_ids)?;
|
|
||||||
insert_document_correspondents(
|
|
||||||
&mut conn,
|
|
||||||
tenant_id,
|
|
||||||
document.id,
|
|
||||||
user_id,
|
|
||||||
&correspondent_ids,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if document.deleted_at.is_some() {
|
|
||||||
let now = Utc::now().naive_utc();
|
|
||||||
diesel::update(documents::table.find(document.id))
|
|
||||||
.set((
|
|
||||||
documents::deleted_at.eq(None::<NaiveDateTime>),
|
|
||||||
documents::updated_at.eq(now),
|
|
||||||
))
|
|
||||||
.execute(&mut conn)?;
|
|
||||||
document.deleted_at = None;
|
|
||||||
document.updated_at = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags_and_correspondents =
|
|
||||||
load_tags_and_correspondents(&mut conn, &[document.id])?;
|
|
||||||
let (tags, correspondents) = tags_and_correspondents
|
|
||||||
.get(&document.id)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| (Vec::new(), Vec::new()));
|
|
||||||
let assets = load_asset_responses(self.state, tenant_id, version.id).await?;
|
|
||||||
let version_response = to_version_response(version.clone());
|
|
||||||
|
|
||||||
info!(
|
|
||||||
document_id = %document.id,
|
|
||||||
checksum = %checksum_hex,
|
|
||||||
"upload deduplicated existing document"
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(DocumentUploadOutcome::Reused(DocumentDetailResponse {
|
|
||||||
document: self.to_document_response(
|
|
||||||
user_id,
|
|
||||||
document,
|
|
||||||
tags,
|
|
||||||
correspondents,
|
|
||||||
Some((version_response, assets)),
|
|
||||||
)?,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_disposition = inline_content_disposition(&stored_filename);
|
let content_disposition = inline_content_disposition(&stored_filename);
|
||||||
@@ -803,66 +719,61 @@ impl<'a> DocumentsService<'a> {
|
|||||||
metadata
|
metadata
|
||||||
};
|
};
|
||||||
|
|
||||||
let (document, version) = {
|
let (document, version) = match tenant_conn.transaction(|conn| {
|
||||||
let mut conn = self.state.db_for_tenant(tenant_id)?;
|
let new_document = NewDocument {
|
||||||
let transaction_result = conn.transaction(|conn| {
|
id: doc_id,
|
||||||
let new_document = NewDocument {
|
filename: stored_filename.clone(),
|
||||||
id: doc_id,
|
original_name: original_name.clone(),
|
||||||
filename: stored_filename.clone(),
|
content_type: content_type.clone(),
|
||||||
original_name: original_name.clone(),
|
folder_id,
|
||||||
content_type: content_type.clone(),
|
current_version_id: version_id,
|
||||||
folder_id,
|
metadata: metadata_value.clone(),
|
||||||
current_version_id: version_id,
|
issued_at: issued_at_override,
|
||||||
metadata: metadata_value.clone(),
|
title: derived_title.clone(),
|
||||||
issued_at: issued_at_override,
|
tenant_id,
|
||||||
title: derived_title.clone(),
|
};
|
||||||
tenant_id,
|
diesel::insert_into(documents::table)
|
||||||
};
|
.values(&new_document)
|
||||||
diesel::insert_into(documents::table)
|
.execute(conn)?;
|
||||||
.values(&new_document)
|
|
||||||
.execute(conn)?;
|
|
||||||
|
|
||||||
let new_version = NewDocumentVersion {
|
let new_version = NewDocumentVersion {
|
||||||
id: version_id,
|
id: version_id,
|
||||||
document_id: doc_id,
|
document_id: doc_id,
|
||||||
version_number,
|
version_number,
|
||||||
s3_key: s3_key.clone(),
|
s3_key: s3_key.clone(),
|
||||||
size_bytes,
|
size_bytes,
|
||||||
checksum: checksum_hex.clone(),
|
checksum: checksum_hex.clone(),
|
||||||
metadata: Value::Object(Default::default()),
|
metadata: Value::Object(Default::default()),
|
||||||
tenant_id,
|
tenant_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
diesel::insert_into(document_versions::table)
|
diesel::insert_into(document_versions::table)
|
||||||
.values(&new_version)
|
.values(&new_version)
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
|
||||||
let document: Document = documents::table.find(doc_id).first(conn)?;
|
let document: Document = documents::table.find(doc_id).first(conn)?;
|
||||||
let version: DocumentVersion =
|
let version: DocumentVersion = document_versions::table.find(version_id).first(conn)?;
|
||||||
document_versions::table.find(version_id).first(conn)?;
|
|
||||||
|
|
||||||
Ok::<_, diesel::result::Error>((document, version))
|
Ok::<_, diesel::result::Error>((document, version))
|
||||||
});
|
}) {
|
||||||
|
Ok(result) => result,
|
||||||
match transaction_result {
|
Err(diesel::result::Error::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => {
|
||||||
Ok(result) => result,
|
return Err(AppError::conflict(
|
||||||
Err(diesel::result::Error::DatabaseError(
|
"another document in this folder already uses that filename",
|
||||||
DatabaseErrorKind::UniqueViolation,
|
)
|
||||||
_,
|
.with_code("duplicate_filename"))
|
||||||
)) => {
|
|
||||||
return Err(AppError::conflict(
|
|
||||||
"another document in this folder already uses that filename",
|
|
||||||
)
|
|
||||||
.with_code("duplicate_filename"))
|
|
||||||
}
|
|
||||||
Err(err) => return Err(AppError::from(err)),
|
|
||||||
}
|
}
|
||||||
|
Err(err) => return Err(AppError::from(err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let detail = {
|
let detail = {
|
||||||
let mut conn = self.state.db_for_tenant(tenant_id)?;
|
assign_tags_to_document(
|
||||||
|
&mut tenant_conn,
|
||||||
assign_tags_to_document(&mut conn, tenant_id, &document, &tag_ids, Some(user_id))?;
|
tenant_id,
|
||||||
|
&document,
|
||||||
|
&tag_ids,
|
||||||
|
Some(user_id),
|
||||||
|
)?;
|
||||||
|
|
||||||
if !correspondents.is_empty() {
|
if !correspondents.is_empty() {
|
||||||
let raw_ids: Vec<Uuid> = correspondents
|
let raw_ids: Vec<Uuid> = correspondents
|
||||||
@@ -871,7 +782,7 @@ impl<'a> DocumentsService<'a> {
|
|||||||
.collect();
|
.collect();
|
||||||
let correspondent_ids = normalize_correspondent_ids(&raw_ids)?;
|
let correspondent_ids = normalize_correspondent_ids(&raw_ids)?;
|
||||||
insert_document_correspondents(
|
insert_document_correspondents(
|
||||||
&mut conn,
|
&mut tenant_conn,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
document.id,
|
document.id,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -879,7 +790,8 @@ impl<'a> DocumentsService<'a> {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags_and_correspondents = load_tags_and_correspondents(&mut conn, &[doc_id])?;
|
let tags_and_correspondents =
|
||||||
|
load_tags_and_correspondents(&mut tenant_conn, &[doc_id])?;
|
||||||
let (tags, correspondents) = tags_and_correspondents
|
let (tags, correspondents) = tags_and_correspondents
|
||||||
.get(&doc_id)
|
.get(&doc_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -896,22 +808,18 @@ impl<'a> DocumentsService<'a> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(mut conn) = self.state.db_for_tenant(tenant_id) {
|
if let Err(err) = enqueue_job(
|
||||||
if let Err(err) = enqueue_job(
|
&mut tenant_conn,
|
||||||
&mut conn,
|
tenant_id,
|
||||||
tenant_id,
|
JOB_ANALYZE_DOCUMENT,
|
||||||
JOB_ANALYZE_DOCUMENT,
|
json!({
|
||||||
json!({
|
"document_id": doc_id,
|
||||||
"document_id": doc_id,
|
"document_version_id": version.id,
|
||||||
"document_version_id": version.id,
|
"force": false,
|
||||||
"force": false,
|
}),
|
||||||
}),
|
None,
|
||||||
None,
|
) {
|
||||||
) {
|
warn!(document_id = %doc_id, error = %err, "failed to enqueue analyze job");
|
||||||
warn!(document_id = %doc_id, error = %err, "failed to enqueue analyze job");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!(document_id = %doc_id, "failed to enqueue analyze job due to pool error");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(DocumentUploadOutcome::Created(detail))
|
Ok(DocumentUploadOutcome::Created(detail))
|
||||||
@@ -1106,7 +1014,7 @@ impl<'a> DocumentsService<'a> {
|
|||||||
.filter(document_versions::tenant_id.eq(tenant_id))
|
.filter(document_versions::tenant_id.eq(tenant_id))
|
||||||
.first(conn)?;
|
.first(conn)?;
|
||||||
|
|
||||||
let assets = load_asset_responses(self.state, tenant_id, version.id).await?;
|
let assets = load_asset_responses_with_conn(conn, tenant_id, version.id)?;
|
||||||
let download_path = build_download_path(self.state, &document, user_id)?;
|
let download_path = build_download_path(self.state, &document, user_id)?;
|
||||||
let version_core = to_version_response(version);
|
let version_core = to_version_response(version);
|
||||||
|
|
||||||
@@ -1450,6 +1358,120 @@ impl<'a> DocumentsService<'a> {
|
|||||||
current_version,
|
current_version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn try_reuse_existing_document(
|
||||||
|
&self,
|
||||||
|
conn: &mut PgPooledConnection,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
checksum_hex: &str,
|
||||||
|
issued_at_override: Option<NaiveDateTime>,
|
||||||
|
skip_if_existing: bool,
|
||||||
|
tag_ids: &[Uuid],
|
||||||
|
correspondents: &[CorrespondentAssignmentInput],
|
||||||
|
) -> AppResult<Option<DocumentDetailResponse>> {
|
||||||
|
let existing = documents::table
|
||||||
|
.inner_join(
|
||||||
|
document_versions::table
|
||||||
|
.on(document_versions::id.eq(documents::current_version_id)),
|
||||||
|
)
|
||||||
|
.filter(documents::tenant_id.eq(tenant_id))
|
||||||
|
.filter(document_versions::checksum.eq(checksum_hex))
|
||||||
|
.select((documents::all_columns, document_versions::all_columns))
|
||||||
|
.first::<(Document, DocumentVersion)>(conn)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
let Some((mut document, version)) = existing else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if skip_if_existing {
|
||||||
|
info!(
|
||||||
|
document_id = %document.id,
|
||||||
|
checksum = %checksum_hex,
|
||||||
|
"upload rejected because document already exists",
|
||||||
|
);
|
||||||
|
return Err(
|
||||||
|
AppError::conflict("a document with the same contents already exists")
|
||||||
|
.with_code("duplicate_document")
|
||||||
|
.with_details(json!({
|
||||||
|
"conflict_document_id": document.id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(issued_at) = issued_at_override {
|
||||||
|
if document.issued_at != Some(issued_at) {
|
||||||
|
diesel::update(
|
||||||
|
documents::table
|
||||||
|
.find(document.id)
|
||||||
|
.filter(documents::tenant_id.eq(tenant_id)),
|
||||||
|
)
|
||||||
|
.set((
|
||||||
|
documents::issued_at.eq(Some(issued_at)),
|
||||||
|
documents::updated_at.eq(Utc::now().naive_utc()),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
document.issued_at = Some(issued_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign_tags_to_document(conn, tenant_id, &document, tag_ids, Some(user_id))?;
|
||||||
|
|
||||||
|
if !correspondents.is_empty() {
|
||||||
|
let raw_ids: Vec<Uuid> = correspondents
|
||||||
|
.iter()
|
||||||
|
.map(|assignment| assignment.correspondent_id)
|
||||||
|
.collect();
|
||||||
|
let correspondent_ids = normalize_correspondent_ids(&raw_ids)?;
|
||||||
|
insert_document_correspondents(
|
||||||
|
conn,
|
||||||
|
tenant_id,
|
||||||
|
document.id,
|
||||||
|
user_id,
|
||||||
|
&correspondent_ids,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if document.deleted_at.is_some() {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
diesel::update(documents::table.find(document.id))
|
||||||
|
.set((
|
||||||
|
documents::deleted_at.eq(None::<NaiveDateTime>),
|
||||||
|
documents::updated_at.eq(now),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
document.deleted_at = None;
|
||||||
|
document.updated_at = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
let relations = load_tags_and_correspondents(conn, &[document.id])?;
|
||||||
|
let (tags, correspondents_list) = relations
|
||||||
|
.get(&document.id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| (Vec::new(), Vec::new()));
|
||||||
|
|
||||||
|
let assets = load_asset_responses_with_conn(conn, tenant_id, version.id)?;
|
||||||
|
let version_response = to_version_response(version.clone());
|
||||||
|
|
||||||
|
info!(
|
||||||
|
document_id = %document.id,
|
||||||
|
checksum = %checksum_hex,
|
||||||
|
"upload deduplicated existing document"
|
||||||
|
);
|
||||||
|
|
||||||
|
let detail = DocumentDetailResponse {
|
||||||
|
document: self.to_document_response(
|
||||||
|
user_id,
|
||||||
|
document,
|
||||||
|
tags,
|
||||||
|
correspondents_list,
|
||||||
|
Some((version_response, assets)),
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(detail))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn presign_disposition_for_asset(
|
fn presign_disposition_for_asset(
|
||||||
|
|||||||
Reference in New Issue
Block a user