Skip to content

Commit 45b7321

Browse files
Merge pull request #278 from yourssu/fix/277
[#277] fix: S3 메일 파일 key 정규화로 NoSuchKey 방지
2 parents 38f1bdf + 46e5df1 commit 45b7321

2 files changed

Lines changed: 73 additions & 8 deletions

File tree

src/main/kotlin/com/yourssu/scouter/common/implement/support/storage/S3MailFileStorage.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ class S3MailFileStorage(
3434
bytes: ByteArray,
3535
contentType: String,
3636
): String {
37+
val resolvedKey = resolveKey(key)
3738
return try {
38-
val resolvedKey = resolveKey(key)
3939
val request =
4040
PutObjectRequest.builder()
4141
.bucket(properties.bucket)
@@ -46,13 +46,16 @@ class S3MailFileStorage(
4646

4747
resolvedKey
4848
} catch (e: Exception) {
49-
throw IllegalStateException("S3 파일 업로드에 실패했습니다: $key", e)
49+
throw IllegalStateException(
50+
"S3 파일 업로드에 실패했습니다. bucket=${properties.bucket}, key=$key, resolvedKey=$resolvedKey",
51+
e,
52+
)
5053
}
5154
}
5255

5356
override fun download(key: String): ByteArray {
57+
val resolvedKey = resolveKey(key)
5458
return try {
55-
val resolvedKey = resolveKey(key)
5659
val request =
5760
GetObjectRequest.builder()
5861
.bucket(properties.bucket)
@@ -61,7 +64,10 @@ class S3MailFileStorage(
6164

6265
client.getObjectAsBytes(request).asByteArray()
6366
} catch (e: Exception) {
64-
throw IllegalStateException("S3 파일 다운로드에 실패했습니다: $key", e)
67+
throw IllegalStateException(
68+
"S3 파일 다운로드에 실패했습니다. bucket=${properties.bucket}, key=$key, resolvedKey=$resolvedKey",
69+
e,
70+
)
6571
}
6672
}
6773

@@ -70,8 +76,8 @@ class S3MailFileStorage(
7076
contentType: String,
7177
expireDuration: Duration,
7278
): String {
79+
val resolvedKey = resolveKey(key)
7380
return try {
74-
val resolvedKey = resolveKey(key)
7581
val putObjectRequest =
7682
PutObjectRequest.builder()
7783
.bucket(properties.bucket)
@@ -86,7 +92,10 @@ class S3MailFileStorage(
8692

8793
presigner.presignPutObject(presignRequest).url().toString()
8894
} catch (e: Exception) {
89-
throw IllegalStateException("S3 업로드용 presigned URL 생성에 실패했습니다: $key", e)
95+
throw IllegalStateException(
96+
"S3 업로드용 presigned URL 생성에 실패했습니다. bucket=${properties.bucket}, key=$key, resolvedKey=$resolvedKey",
97+
e,
98+
)
9099
}
91100
}
92101

@@ -96,11 +105,17 @@ class S3MailFileStorage(
96105
}
97106

98107
private fun resolveKey(key: String): String {
108+
val normalizedKey = key.trim().removePrefix("/")
99109
if (properties.keyPrefix.isBlank()) {
100-
return key
110+
return normalizedKey
111+
}
112+
113+
val normalizedPrefix = properties.keyPrefix.trim().trim('/')
114+
if (normalizedKey == normalizedPrefix || normalizedKey.startsWith("$normalizedPrefix/")) {
115+
return normalizedKey
101116
}
102117

103-
return "${properties.keyPrefix.trimEnd('/')}/$key"
118+
return "$normalizedPrefix/$normalizedKey"
104119
}
105120

106121
@PreDestroy
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.yourssu.scouter.common.implement.support.storage
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.jupiter.api.Test
5+
6+
@Suppress("NonAsciiCharacters")
7+
class S3MailFileStorageKeyResolutionTest {
8+
private fun createStorage(prefix: String): S3MailFileStorage {
9+
return S3MailFileStorage(
10+
MailS3StorageProperties(
11+
bucket = "test-bucket",
12+
region = "ap-northeast-2",
13+
keyPrefix = prefix,
14+
),
15+
)
16+
}
17+
18+
@Test
19+
fun `getPublicUrl은 상대 key에 prefix를 1회만 붙인다`() {
20+
val storage = createStorage("dev/mail-files")
21+
22+
val url = storage.getPublicUrl("attachment/2/sample.png")
23+
24+
assertThat(url)
25+
.isEqualTo("https://test-bucket.s3.ap-northeast-2.amazonaws.com/dev/mail-files/attachment/2/sample.png")
26+
storage.close()
27+
}
28+
29+
@Test
30+
fun `getPublicUrl은 이미 prefix가 포함된 key를 중복 prefix하지 않는다`() {
31+
val storage = createStorage("dev/mail-files")
32+
33+
val url = storage.getPublicUrl("dev/mail-files/attachment/2/sample.png")
34+
35+
assertThat(url)
36+
.isEqualTo("https://test-bucket.s3.ap-northeast-2.amazonaws.com/dev/mail-files/attachment/2/sample.png")
37+
storage.close()
38+
}
39+
40+
@Test
41+
fun `getPublicUrl은 선행 슬래시가 있어도 정상 key로 정규화한다`() {
42+
val storage = createStorage("dev/mail-files")
43+
44+
val url = storage.getPublicUrl("/dev/mail-files/attachment/2/sample.png")
45+
46+
assertThat(url)
47+
.isEqualTo("https://test-bucket.s3.ap-northeast-2.amazonaws.com/dev/mail-files/attachment/2/sample.png")
48+
storage.close()
49+
}
50+
}

0 commit comments

Comments
 (0)