| 8 |
8 |
|
use std::str::FromStr;
|
| 9 |
9 |
|
|
| 10 |
10 |
|
use crate::config::StorageConfig;
|
| 11 |
|
- |
use crate::db::{ItemId, ProjectId, UserId, VersionId};
|
|
11 |
+ |
use crate::db::{ItemId, ProjectId, SyncAppId, UserId, VersionId};
|
| 12 |
12 |
|
use crate::error::{AppError, Result};
|
| 13 |
13 |
|
|
|
14 |
+ |
/// A storage object key. There are exactly two ways to obtain one, and an
|
|
15 |
+ |
/// ad-hoc `format!("...")` is neither:
|
|
16 |
+ |
///
|
|
17 |
+ |
/// 1. A `S3Client::generate_*` constructor — the single, reviewed home for key
|
|
18 |
+ |
/// *layout*. Multi-instance kinds (versions, gallery, media) take their
|
|
19 |
+ |
/// uniqueness segment (a table PK or a fresh uuid) as a required argument, so
|
|
20 |
+ |
/// a collidable key cannot be built; singleton kinds (audio/cover/video, OTA
|
|
21 |
+ |
/// artifacts) are one-per-parent and correctly overwrite-on-replace.
|
|
22 |
+ |
/// 2. [`S3Key::from_stored`] — the named trust boundary for a key that already
|
|
23 |
+ |
/// exists in our storage (read back from a DB row). The caller asserts it was
|
|
24 |
+ |
/// minted by a generator at write time; this is how delete/download/re-presign
|
|
25 |
+ |
/// paths address objects without re-deriving their layout.
|
|
26 |
+ |
///
|
|
27 |
+ |
/// Because every write/presign/delete on [`StorageBackend`] takes `&S3Key`, a
|
|
28 |
+ |
/// hand-built string can never reach S3 — the OTA-style inline `format!` key
|
|
29 |
+ |
/// (which bypassed the generators) is now uncompilable.
|
|
30 |
+ |
#[derive(Debug, Clone, PartialEq, Eq, Hash, sqlx::Type)]
|
|
31 |
+ |
#[sqlx(transparent)]
|
|
32 |
+ |
pub struct S3Key(String);
|
|
33 |
+ |
|
|
34 |
+ |
impl S3Key {
|
|
35 |
+ |
/// Wrap a key read back from durable storage (a DB row). Names the trust
|
|
36 |
+ |
/// boundary: the caller asserts this key was minted by a `generate_*`
|
|
37 |
+ |
/// constructor when the object was written, not freshly invented here.
|
|
38 |
+ |
pub fn from_stored(key: impl AsRef<str>) -> Self {
|
|
39 |
+ |
S3Key(key.as_ref().to_string())
|
|
40 |
+ |
}
|
|
41 |
+ |
|
|
42 |
+ |
pub fn as_str(&self) -> &str {
|
|
43 |
+ |
&self.0
|
|
44 |
+ |
}
|
|
45 |
+ |
|
|
46 |
+ |
pub fn into_string(self) -> String {
|
|
47 |
+ |
self.0
|
|
48 |
+ |
}
|
|
49 |
+ |
}
|
|
50 |
+ |
|
|
51 |
+ |
impl std::ops::Deref for S3Key {
|
|
52 |
+ |
type Target = str;
|
|
53 |
+ |
fn deref(&self) -> &str {
|
|
54 |
+ |
&self.0
|
|
55 |
+ |
}
|
|
56 |
+ |
}
|
|
57 |
+ |
|
|
58 |
+ |
impl AsRef<str> for S3Key {
|
|
59 |
+ |
fn as_ref(&self) -> &str {
|
|
60 |
+ |
&self.0
|
|
61 |
+ |
}
|
|
62 |
+ |
}
|
|
63 |
+ |
|
|
64 |
+ |
impl std::fmt::Display for S3Key {
|
|
65 |
+ |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
66 |
+ |
f.write_str(&self.0)
|
|
67 |
+ |
}
|
|
68 |
+ |
}
|
|
69 |
+ |
|
|
70 |
+ |
impl PartialEq<&str> for S3Key {
|
|
71 |
+ |
fn eq(&self, other: &&str) -> bool {
|
|
72 |
+ |
self.0 == *other
|
|
73 |
+ |
}
|
|
74 |
+ |
}
|
|
75 |
+ |
|
| 14 |
76 |
|
/// Allowed audio file extensions and their MIME types
|
| 15 |
77 |
|
const ALLOWED_AUDIO_TYPES: &[(&str, &str)] = &[
|
| 16 |
78 |
|
("mp3", "audio/mpeg"),
|
| 246 |
308 |
|
/// the URL as `Content-Length` so S3 itself enforces the size cap at the
|
| 247 |
309 |
|
/// protocol level (prevents oversized PUTs from burning bandwidth before
|
| 248 |
310 |
|
/// hitting the post-PUT delete-and-charge fallback).
|
| 249 |
|
- |
async fn presign_upload(&self, s3_key: &str, content_type: &str, expiry_secs: Option<u64>, cache_control: Option<&str>, max_bytes: Option<i64>) -> Result<String>;
|
| 250 |
|
- |
async fn presign_download(&self, s3_key: &str, expiry_secs: Option<u64>) -> Result<String>;
|
|
311 |
+ |
async fn presign_upload(&self, s3_key: &S3Key, content_type: &str, expiry_secs: Option<u64>, cache_control: Option<&str>, max_bytes: Option<i64>) -> Result<String>;
|
|
312 |
+ |
async fn presign_download(&self, s3_key: &S3Key, expiry_secs: Option<u64>) -> Result<String>;
|
| 251 |
313 |
|
async fn object_exists(&self, s3_key: &str) -> Result<bool>;
|
| 252 |
314 |
|
async fn object_size(&self, s3_key: &str) -> Result<Option<i64>>;
|
| 253 |
315 |
|
async fn download_object(&self, s3_key: &str) -> Result<Vec<u8>>;
|
| 255 |
317 |
|
/// drive the stream to disk (scanner spool) or to a layer that consumes
|
| 256 |
318 |
|
/// chunks directly.
|
| 257 |
319 |
|
async fn download_stream(&self, s3_key: &str) -> Result<s3_storage::ByteStream>;
|
| 258 |
|
- |
async fn upload_object(&self, s3_key: &str, content_type: &str, data: Vec<u8>, cache_control: Option<&str>) -> Result<()>;
|
|
320 |
+ |
async fn upload_object(&self, s3_key: &S3Key, content_type: &str, data: Vec<u8>, cache_control: Option<&str>) -> Result<()>;
|
| 259 |
321 |
|
/// Delete an object. Requires an [`S3DeleteAuthority`] — route handlers
|
| 260 |
322 |
|
/// cannot mint one, so they must enqueue through `pending_s3_deletions`
|
| 261 |
323 |
|
/// instead of deleting directly (Run #18 CHRONIC B′).
|
| 262 |
|
- |
async fn delete_object(&self, auth: &S3DeleteAuthority, s3_key: &str) -> Result<()>;
|
|
324 |
+ |
async fn delete_object(&self, auth: &S3DeleteAuthority, s3_key: &S3Key) -> Result<()>;
|
| 263 |
325 |
|
/// Delete a batch of objects in a single S3 `DeleteObjects` request
|
| 264 |
326 |
|
/// (up to 1000 keys/call). Default loops `delete_object` so test backends
|
| 265 |
327 |
|
/// don't have to implement it, but production should override.
|
| 266 |
|
- |
async fn delete_objects(&self, auth: &S3DeleteAuthority, keys: &[String]) -> Result<()> {
|
|
328 |
+ |
async fn delete_objects(&self, auth: &S3DeleteAuthority, keys: &[S3Key]) -> Result<()> {
|
| 267 |
329 |
|
for k in keys {
|
| 268 |
330 |
|
if let Err(e) = self.delete_object(auth, k).await {
|
| 269 |
331 |
|
tracing::warn!(key = %k, error = ?e, "delete_objects: per-key delete failed");
|
| 280 |
342 |
|
/// default that `tokio::fs::read`s the whole file into RAM + single PUT
|
| 281 |
343 |
|
/// silently defeats streaming, so a future backend that forgot to override
|
| 282 |
344 |
|
/// it would quietly lose multipart. Every backend must declare its strategy.
|
| 283 |
|
- |
async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()>;
|
|
345 |
+ |
async fn upload_multipart(&self, s3_key: &S3Key, content_type: &str, file_path: &std::path::Path) -> Result<()>;
|
| 284 |
346 |
|
async fn check_connectivity(&self) -> std::result::Result<(), String>;
|
| 285 |
347 |
|
fn bucket(&self) -> &str;
|
| 286 |
348 |
|
}
|
| 322 |
384 |
|
item_id: ItemId,
|
| 323 |
385 |
|
file_type: FileType,
|
| 324 |
386 |
|
filename: &str,
|
| 325 |
|
- |
) -> String {
|
|
387 |
+ |
) -> S3Key {
|
| 326 |
388 |
|
let safe_filename = sanitize_filename(filename);
|
| 327 |
|
- |
format!(
|
|
389 |
+ |
S3Key(format!(
|
| 328 |
390 |
|
"{}/{}/{}/{}",
|
| 329 |
391 |
|
user_id,
|
| 330 |
392 |
|
item_id,
|
| 331 |
393 |
|
file_type.as_str(),
|
| 332 |
394 |
|
safe_filename
|
| 333 |
|
- |
)
|
|
395 |
+ |
))
|
| 334 |
396 |
|
}
|
| 335 |
397 |
|
|
| 336 |
398 |
|
/// Generate an S3 key for a version download file. The version's own id is
|
| 345 |
407 |
|
item_id: ItemId,
|
| 346 |
408 |
|
version_id: VersionId,
|
| 347 |
409 |
|
filename: &str,
|
| 348 |
|
- |
) -> String {
|
|
410 |
+ |
) -> S3Key {
|
| 349 |
411 |
|
let safe_filename = sanitize_filename(filename);
|
| 350 |
|
- |
format!(
|
|
412 |
+ |
S3Key(format!(
|
| 351 |
413 |
|
"{}/{}/{}/{}/{}",
|
| 352 |
414 |
|
user_id,
|
| 353 |
415 |
|
item_id,
|
| 354 |
416 |
|
FileType::Download.as_str(),
|
| 355 |
417 |
|
version_id,
|
| 356 |
418 |
|
safe_filename
|
| 357 |
|
- |
)
|
|
419 |
+ |
))
|
| 358 |
420 |
|
}
|
| 359 |
421 |
|
|
| 360 |
422 |
|
/// Generate an S3 key for a reusable insertion clip (not tied to any item).
|
| 361 |
423 |
|
/// Format: {user_id}/insertions/{filename}
|
| 362 |
|
- |
pub fn generate_insertion_key(user_id: UserId, filename: &str) -> String {
|
|
424 |
+ |
pub fn generate_insertion_key(user_id: UserId, filename: &str) -> S3Key {
|
| 363 |
425 |
|
let safe_filename = sanitize_filename(filename);
|
| 364 |
|
- |
format!("{}/insertions/{}", user_id, safe_filename)
|
|
426 |
+ |
S3Key(format!("{}/insertions/{}", user_id, safe_filename))
|
| 365 |
427 |
|
}
|
| 366 |
428 |
|
|
| 367 |
429 |
|
/// Generate an S3 key for a media library file.
|
| 368 |
430 |
|
/// Format: `{user_id}/media/{folder}/{filename}` (or `{user_id}/media/{filename}` for root folder).
|
| 369 |
|
- |
pub fn generate_media_key(user_id: UserId, folder: &str, filename: &str) -> String {
|
|
431 |
+ |
pub fn generate_media_key(user_id: UserId, folder: &str, filename: &str) -> S3Key {
|
| 370 |
432 |
|
let safe_filename = sanitize_filename(filename);
|
| 371 |
433 |
|
let safe_folder = sanitize_folder(folder);
|
| 372 |
434 |
|
if safe_folder.is_empty() {
|
| 373 |
|
- |
format!("{}/media/{}", user_id, safe_filename)
|
|
435 |
+ |
S3Key(format!("{}/media/{}", user_id, safe_filename))
|
| 374 |
436 |
|
} else {
|
| 375 |
|
- |
format!("{}/media/{}/{}", user_id, safe_folder, safe_filename)
|
|
437 |
+ |
S3Key(format!("{}/media/{}/{}", user_id, safe_folder, safe_filename))
|
| 376 |
438 |
|
}
|
| 377 |
439 |
|
}
|
| 378 |
440 |
|
|
| 379 |
441 |
|
/// Generate an S3 key for a project image (logo/avatar).
|
| 380 |
442 |
|
/// Format: projects/{project_id}/image/{sanitized_filename}
|
| 381 |
|
- |
pub fn generate_project_image_key(project_id: ProjectId, filename: &str) -> String {
|
|
443 |
+ |
pub fn generate_project_image_key(project_id: ProjectId, filename: &str) -> S3Key {
|
| 382 |
444 |
|
let safe_filename = sanitize_filename(filename);
|
| 383 |
|
- |
format!("projects/{}/image/{}", project_id, safe_filename)
|
|
445 |
+ |
S3Key(format!("projects/{}/image/{}", project_id, safe_filename))
|
|
446 |
+ |
}
|
|
447 |
+ |
|
|
448 |
+ |
/// Generate an S3 key for an OTA release artifact. Singleton per
|
|
449 |
+ |
/// (app, version, target, arch) — the release row already enforces
|
|
450 |
+ |
/// `UNIQUE(app_id, version)` and the artifact row `UNIQUE(release_id, target,
|
|
451 |
+ |
/// arch)`, so re-uploading the same artifact correctly overwrites in place.
|
|
452 |
+ |
/// Centralized here so OTA keys are no longer hand-built at the call site.
|
|
453 |
+ |
/// Format: ota/{app_id}/{version}/{target}/{arch}/artifact
|
|
454 |
+ |
pub fn generate_ota_artifact_key(
|
|
455 |
+ |
app_id: SyncAppId,
|
|
456 |
+ |
version: &str,
|
|
457 |
+ |
target: &str,
|
|
458 |
+ |
arch: &str,
|
|
459 |
+ |
) -> S3Key {
|
|
460 |
+ |
S3Key(format!("ota/{app_id}/{version}/{target}/{arch}/artifact"))
|
|
461 |
+ |
}
|
|
462 |
+ |
|
|
463 |
+ |
/// Generate an S3 key for a SyncKit content-addressed blob. The hash is the
|
|
464 |
+ |
/// uniqueness segment (and `UNIQUE(app_id, user_id, hash)` backs it), so two
|
|
465 |
+ |
/// uploads of identical bytes resolve to one object by design.
|
|
466 |
+ |
/// Format: {app_id}/{user_id}/{hash}
|
|
467 |
+ |
pub fn generate_synckit_blob_key(app_id: SyncAppId, user_id: UserId, hash: &str) -> S3Key {
|
|
468 |
+ |
S3Key(format!("{app_id}/{user_id}/{hash}"))
|
|
469 |
+ |
}
|
|
470 |
+ |
|
|
471 |
+ |
/// Generate an S3 key for a generated content-export archive. Ephemeral
|
|
472 |
+ |
/// (presigned, then reaped); the timestamp keeps repeat exports distinct.
|
|
473 |
+ |
/// Format: {user_id}/exports/content-{timestamp}.zip
|
|
474 |
+ |
pub fn generate_content_export_key(user_id: UserId, timestamp: &str) -> S3Key {
|
|
475 |
+ |
S3Key(format!("{user_id}/exports/content-{timestamp}.zip"))
|
| 384 |
476 |
|
}
|
| 385 |
477 |
|
|
| 386 |
478 |
|
/// Generate an S3 key for an item gallery image. A per-image uuid segment
|
| 392 |
484 |
|
item_id: ItemId,
|
| 393 |
485 |
|
image_uuid: uuid::Uuid,
|
| 394 |
486 |
|
filename: &str,
|
| 395 |
|
- |
) -> String {
|
|
487 |
+ |
) -> S3Key {
|
| 396 |
488 |
|
let safe_filename = sanitize_filename(filename);
|
| 397 |
|
- |
format!("{}/{}/gallery/{}/{}", user_id, item_id, image_uuid, safe_filename)
|
|
489 |
+ |
S3Key(format!("{}/{}/gallery/{}/{}", user_id, item_id, image_uuid, safe_filename))
|
| 398 |
490 |
|
}
|
| 399 |
491 |
|
|
| 400 |
492 |
|
/// Generate an S3 key for a project gallery image.
|
| 403 |
495 |
|
project_id: ProjectId,
|
| 404 |
496 |
|
image_uuid: uuid::Uuid,
|
| 405 |
497 |
|
filename: &str,
|
| 406 |
|
- |
) -> String {
|
|
498 |
+ |
) -> S3Key {
|
| 407 |
499 |
|
let safe_filename = sanitize_filename(filename);
|
| 408 |
|
- |
format!("projects/{}/gallery/{}/{}", project_id, image_uuid, safe_filename)
|
|
500 |
+ |
S3Key(format!("projects/{}/gallery/{}/{}", project_id, image_uuid, safe_filename))
|
| 409 |
501 |
|
}
|
| 410 |
502 |
|
|
| 411 |
503 |
|
/// Validate content type for the given file type
|
| 478 |
570 |
|
/// whose actual body length differs from `max_bytes`.
|
| 479 |
571 |
|
pub async fn presign_upload(
|
| 480 |
572 |
|
&self,
|
| 481 |
|
- |
s3_key: &str,
|
|
573 |
+ |
s3_key: &S3Key,
|
| 482 |
574 |
|
content_type: &str,
|
| 483 |
575 |
|
expiry_secs: Option<u64>,
|
| 484 |
576 |
|
cache_control: Option<&str>,
|
| 485 |
577 |
|
max_bytes: Option<i64>,
|
| 486 |
578 |
|
) -> Result<String> {
|
| 487 |
579 |
|
self.inner
|
| 488 |
|
- |
.presign_upload(s3_key, content_type, expiry_secs.unwrap_or(PRESIGN_EXPIRY_SECS), cache_control, max_bytes)
|
|
580 |
+ |
.presign_upload(s3_key.as_str(), content_type, expiry_secs.unwrap_or(PRESIGN_EXPIRY_SECS), cache_control, max_bytes)
|
| 489 |
581 |
|
.await
|
| 490 |
582 |
|
.map_err(AppError::Storage)
|
| 491 |
583 |
|
}
|
| 493 |
585 |
|
/// Generate a presigned URL for downloading/streaming a file
|
| 494 |
586 |
|
pub async fn presign_download(
|
| 495 |
587 |
|
&self,
|
| 496 |
|
- |
s3_key: &str,
|
|
588 |
+ |
s3_key: &S3Key,
|
| 497 |
589 |
|
expiry_secs: Option<u64>,
|
| 498 |
590 |
|
) -> Result<String> {
|
| 499 |
591 |
|
self.inner
|
| 500 |
|
- |
.presign_download(s3_key, expiry_secs.unwrap_or(PRESIGN_EXPIRY_SECS))
|
|
592 |
+ |
.presign_download(s3_key.as_str(), expiry_secs.unwrap_or(PRESIGN_EXPIRY_SECS))
|
| 501 |
593 |
|
.await
|
| 502 |
594 |
|
.map_err(AppError::Storage)
|
| 503 |
595 |
|
}
|
| 532 |
624 |
|
/// Upload an object to S3 from bytes
|
| 533 |
625 |
|
pub async fn upload_object(
|
| 534 |
626 |
|
&self,
|
| 535 |
|
- |
s3_key: &str,
|
|
627 |
+ |
s3_key: &S3Key,
|
| 536 |
628 |
|
content_type: &str,
|
| 537 |
629 |
|
data: Vec<u8>,
|
| 538 |
630 |
|
cache_control: Option<&str>,
|
| 539 |
631 |
|
) -> Result<()> {
|
| 540 |
632 |
|
self.inner
|
| 541 |
|
- |
.upload(s3_key, content_type, data, cache_control)
|
|
633 |
+ |
.upload(s3_key.as_str(), content_type, data, cache_control)
|
| 542 |
634 |
|
.await
|
| 543 |
635 |
|
.map_err(AppError::Storage)
|
| 544 |
636 |
|
}
|
| 545 |
637 |
|
|
| 546 |
638 |
|
/// Delete an object from S3
|
| 547 |
|
- |
pub async fn delete_object(&self, s3_key: &str) -> Result<()> {
|
| 548 |
|
- |
self.inner.delete(s3_key).await.map_err(AppError::Storage)
|
|
639 |
+ |
pub async fn delete_object(&self, s3_key: &S3Key) -> Result<()> {
|
|
640 |
+ |
self.inner.delete(s3_key.as_str()).await.map_err(AppError::Storage)
|
| 549 |
641 |
|
}
|
| 550 |
642 |
|
|
| 551 |
643 |
|
/// Batched S3 delete (`DeleteObjects`, up to 1000 keys per call).
|
| 552 |
644 |
|
/// Chunks larger slices into 1000-key batches and logs per-key failures
|
| 553 |
645 |
|
/// without bubbling — the pending_s3_deletions queue is the safety net.
|
| 554 |
|
- |
pub async fn delete_objects(&self, keys: &[String]) -> Result<()> {
|
|
646 |
+ |
pub async fn delete_objects(&self, keys: &[S3Key]) -> Result<()> {
|
| 555 |
647 |
|
if keys.is_empty() {
|
| 556 |
648 |
|
return Ok(());
|
| 557 |
649 |
|
}
|
| 558 |
650 |
|
for chunk in keys.chunks(1000) {
|
| 559 |
|
- |
match self.inner.delete_objects(chunk).await {
|
|
651 |
+ |
let chunk: Vec<String> = chunk.iter().map(|k| k.as_str().to_string()).collect();
|
|
652 |
+ |
match self.inner.delete_objects(&chunk).await {
|
| 560 |
653 |
|
Ok(failures) => {
|
| 561 |
654 |
|
for (k, msg) in failures {
|
| 562 |
655 |
|
tracing::warn!(key = %k, error = %msg, "S3 delete_objects: key-level failure");
|
| 569 |
662 |
|
}
|
| 570 |
663 |
|
|
| 571 |
664 |
|
/// Upload a file to S3 using multipart upload (10 MB parts).
|
| 572 |
|
- |
pub async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()> {
|
|
665 |
+ |
pub async fn upload_multipart(&self, s3_key: &S3Key, content_type: &str, file_path: &std::path::Path) -> Result<()> {
|
| 573 |
666 |
|
self.inner
|
| 574 |
|
- |
.upload_multipart(s3_key, content_type, file_path, None)
|
|
667 |
+ |
.upload_multipart(s3_key.as_str(), content_type, file_path, None)
|
| 575 |
668 |
|
.await
|
| 576 |
669 |
|
.map_err(AppError::Storage)
|
| 577 |
670 |
|
}
|
| 701 |
794 |
|
if let Some(cdn_base) = cdn_base_url {
|
| 702 |
795 |
|
return Ok(format!("{}/{}", cdn_base, s3_key));
|
| 703 |
796 |
|
}
|
| 704 |
|
- |
s3.presign_download(s3_key, Some(86400)).await
|
|
797 |
+ |
s3.presign_download(&S3Key::from_stored(s3_key), Some(86400)).await
|
| 705 |
798 |
|
}
|
| 706 |
799 |
|
|
| 707 |
800 |
|
#[async_trait::async_trait]
|
| 708 |
801 |
|
impl StorageBackend for S3Client {
|
| 709 |
|
- |
async fn presign_upload(&self, s3_key: &str, content_type: &str, expiry_secs: Option<u64>, cache_control: Option<&str>, max_bytes: Option<i64>) -> Result<String> {
|
|
802 |
+ |
async fn presign_upload(&self, s3_key: &S3Key, content_type: &str, expiry_secs: Option<u64>, cache_control: Option<&str>, max_bytes: Option<i64>) -> Result<String> {
|
| 710 |
803 |
|
self.presign_upload(s3_key, content_type, expiry_secs, cache_control, max_bytes).await
|
| 711 |
804 |
|
}
|
| 712 |
805 |
|
|
| 713 |
|
- |
async fn presign_download(&self, s3_key: &str, expiry_secs: Option<u64>) -> Result<String> {
|
|
806 |
+ |
async fn presign_download(&self, s3_key: &S3Key, expiry_secs: Option<u64>) -> Result<String> {
|
| 714 |
807 |
|
self.presign_download(s3_key, expiry_secs).await
|
| 715 |
808 |
|
}
|
| 716 |
809 |
|
|
| 730 |
823 |
|
self.download_stream(s3_key).await
|
| 731 |
824 |
|
}
|
| 732 |
825 |
|
|
| 733 |
|
- |
async fn upload_object(&self, s3_key: &str, content_type: &str, data: Vec<u8>, cache_control: Option<&str>) -> Result<()> {
|
|
826 |
+ |
async fn upload_object(&self, s3_key: &S3Key, content_type: &str, data: Vec<u8>, cache_control: Option<&str>) -> Result<()> {
|
| 734 |
827 |
|
self.upload_object(s3_key, content_type, data, cache_control).await
|
| 735 |
828 |
|
}
|
| 736 |
829 |
|
|
| 737 |
|
- |
async fn delete_object(&self, _auth: &S3DeleteAuthority, s3_key: &str) -> Result<()> {
|
|
830 |
+ |
async fn delete_object(&self, _auth: &S3DeleteAuthority, s3_key: &S3Key) -> Result<()> {
|
| 738 |
831 |
|
// Authority proven by the caller; delegate to the inherent impl.
|
| 739 |
832 |
|
self.delete_object(s3_key).await
|
| 740 |
833 |
|
}
|
| 741 |
834 |
|
|
| 742 |
|
- |
async fn delete_objects(&self, _auth: &S3DeleteAuthority, keys: &[String]) -> Result<()> {
|
|
835 |
+ |
async fn delete_objects(&self, _auth: &S3DeleteAuthority, keys: &[S3Key]) -> Result<()> {
|
| 743 |
836 |
|
self.delete_objects(keys).await
|
| 744 |
837 |
|
}
|
| 745 |
838 |
|
|
| 748 |
841 |
|
.map_err(AppError::Storage)
|
| 749 |
842 |
|
}
|
| 750 |
843 |
|
|
| 751 |
|
- |
async fn upload_multipart(&self, s3_key: &str, content_type: &str, file_path: &std::path::Path) -> Result<()> {
|
|
844 |
+ |
async fn upload_multipart(&self, s3_key: &S3Key, content_type: &str, file_path: &std::path::Path) -> Result<()> {
|
| 752 |
845 |
|
self.upload_multipart(s3_key, content_type, file_path).await
|
| 753 |
846 |
|
}
|
| 754 |
847 |
|
|