| 8 |
8 |
|
use std::str::FromStr;
|
| 9 |
9 |
|
|
| 10 |
10 |
|
use crate::config::StorageConfig;
|
| 11 |
|
- |
use crate::db::{ItemId, ProjectId, UserId};
|
|
11 |
+ |
use crate::db::{ItemId, ProjectId, UserId, VersionId};
|
| 12 |
12 |
|
use crate::error::{AppError, Result};
|
| 13 |
13 |
|
|
| 14 |
14 |
|
/// Allowed audio file extensions and their MIME types
|
| 333 |
333 |
|
)
|
| 334 |
334 |
|
}
|
| 335 |
335 |
|
|
|
336 |
+ |
/// Generate an S3 key for a version download file. The version's own id is
|
|
337 |
+ |
/// woven into the path so two versions of the same item that share a
|
|
338 |
+ |
/// filename (e.g. a creator who ships every release as `plugin.zip`) never
|
|
339 |
+ |
/// resolve to the same object — mirrors the per-entity-uuid segment the
|
|
340 |
+ |
/// gallery keys use, except the version id is the table's primary key, so
|
|
341 |
+ |
/// uniqueness is guaranteed by construction rather than by a fresh uuid.
|
|
342 |
+ |
/// Format: {user_id}/{item_id}/download/{version_id}/{filename}
|
|
343 |
+ |
pub fn generate_version_key(
|
|
344 |
+ |
user_id: UserId,
|
|
345 |
+ |
item_id: ItemId,
|
|
346 |
+ |
version_id: VersionId,
|
|
347 |
+ |
filename: &str,
|
|
348 |
+ |
) -> String {
|
|
349 |
+ |
let safe_filename = sanitize_filename(filename);
|
|
350 |
+ |
format!(
|
|
351 |
+ |
"{}/{}/{}/{}/{}",
|
|
352 |
+ |
user_id,
|
|
353 |
+ |
item_id,
|
|
354 |
+ |
FileType::Download.as_str(),
|
|
355 |
+ |
version_id,
|
|
356 |
+ |
safe_filename
|
|
357 |
+ |
)
|
|
358 |
+ |
}
|
|
359 |
+ |
|
| 336 |
360 |
|
/// Generate an S3 key for a reusable insertion clip (not tied to any item).
|
| 337 |
361 |
|
/// Format: {user_id}/insertions/{filename}
|
| 338 |
362 |
|
pub fn generate_insertion_key(user_id: UserId, filename: &str) -> String {
|
| 849 |
873 |
|
}
|
| 850 |
874 |
|
|
| 851 |
875 |
|
#[test]
|
|
876 |
+ |
fn test_generate_version_key_is_unique_per_version() {
|
|
877 |
+ |
let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
|
|
878 |
+ |
let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
|
|
879 |
+ |
let v1: VersionId = "33333333-3333-3333-3333-333333333333".parse().unwrap();
|
|
880 |
+ |
let v2: VersionId = "44444444-4444-4444-4444-444444444444".parse().unwrap();
|
|
881 |
+ |
|
|
882 |
+ |
// Two versions of the SAME item sharing a filename must not collide.
|
|
883 |
+ |
let k1 = S3Client::generate_version_key(user_id, item_id, v1, "plugin.zip");
|
|
884 |
+ |
let k2 = S3Client::generate_version_key(user_id, item_id, v2, "plugin.zip");
|
|
885 |
+ |
assert_ne!(k1, k2, "same-filename versions must produce distinct keys");
|
|
886 |
+ |
|
|
887 |
+ |
assert_eq!(
|
|
888 |
+ |
k1,
|
|
889 |
+ |
"11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/download/33333333-3333-3333-3333-333333333333/plugin.zip"
|
|
890 |
+ |
);
|
|
891 |
+ |
// Confirm-handler prefix check is `{user}/{item}/`; the woven key still
|
|
892 |
+ |
// satisfies it.
|
|
893 |
+ |
assert!(k1.starts_with("11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222/"));
|
|
894 |
+ |
// Filename is still the last path segment (confirm extracts it via rsplit).
|
|
895 |
+ |
assert_eq!(k1.rsplit('/').next(), Some("plugin.zip"));
|
|
896 |
+ |
}
|
|
897 |
+ |
|
|
898 |
+ |
#[test]
|
|
899 |
+ |
fn test_generate_version_key_sanitizes_filename() {
|
|
900 |
+ |
let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
|
|
901 |
+ |
let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
|
|
902 |
+ |
let v1: VersionId = "33333333-3333-3333-3333-333333333333".parse().unwrap();
|
|
903 |
+ |
|
|
904 |
+ |
let key = S3Client::generate_version_key(user_id, item_id, v1, "my release (1).zip");
|
|
905 |
+ |
assert!(key.ends_with("/myrelease1.zip"));
|
|
906 |
+ |
}
|
|
907 |
+ |
|
|
908 |
+ |
#[test]
|
| 852 |
909 |
|
fn test_generate_key_sanitizes_filename() {
|
| 853 |
910 |
|
let user_id: UserId = "11111111-1111-1111-1111-111111111111".parse().unwrap();
|
| 854 |
911 |
|
let item_id: ItemId = "22222222-2222-2222-2222-222222222222".parse().unwrap();
|