mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
fix: handle control character object keys (#139)
This commit is contained in:
+8
-1
@@ -18,6 +18,7 @@ import {
|
||||
} from "@aws-sdk/client-s3"
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||
import { useS3 } from "@/contexts/s3-context"
|
||||
import { decodeS3UrlEncodedObjectList, decodeS3UrlEncodedObjectVersions } from "@/lib/s3-object-encoding"
|
||||
|
||||
function attachIncludeDeletedHeader(command: ListObjectsV2Command) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -101,11 +102,13 @@ export function useObject(bucket: string) {
|
||||
MaxKeys: pageSize,
|
||||
Delimiter: "/",
|
||||
ContinuationToken: continuationToken,
|
||||
EncodingType: "url",
|
||||
})
|
||||
if (options?.includeDeleted) {
|
||||
attachIncludeDeletedHeader(command)
|
||||
}
|
||||
return client.send(command)
|
||||
const response = await client.send(command)
|
||||
return decodeS3UrlEncodedObjectList(response)
|
||||
},
|
||||
[client],
|
||||
)
|
||||
@@ -121,8 +124,10 @@ export function useObject(bucket: string) {
|
||||
Bucket: bucketName,
|
||||
Prefix: prefix,
|
||||
ContinuationToken: continuationToken,
|
||||
EncodingType: "url",
|
||||
}),
|
||||
)
|
||||
decodeS3UrlEncodedObjectList(data)
|
||||
|
||||
data.Contents?.forEach((item) => {
|
||||
if (item.Key) callback(item.Key)
|
||||
@@ -256,8 +261,10 @@ export function useObject(bucket: string) {
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
Delimiter: "/",
|
||||
EncodingType: "url",
|
||||
}),
|
||||
)
|
||||
decodeS3UrlEncodedObjectVersions(res)
|
||||
const Versions = (res.Versions ?? []).filter((v) => v.Key === key)
|
||||
const DeleteMarkers = (res.DeleteMarkers ?? []).filter((m) => m.Key === key)
|
||||
Versions.sort((a, b) => new Date(b.LastModified!).getTime() - new Date(a.LastModified!).getTime())
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ListObjectVersionsCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3"
|
||||
import { decodeS3UrlEncodedObjectList, decodeS3UrlEncodedObjectVersions } from "./s3-object-encoding"
|
||||
import type { ManagedTask, TaskHandler, TaskLifecycleStatus } from "./task-manager"
|
||||
import { createTaskId } from "./task-id"
|
||||
|
||||
@@ -116,9 +117,11 @@ export function createDeleteTaskHelpers(s3Client: S3Client, config: DeleteTaskCo
|
||||
Prefix: prefix,
|
||||
KeyMarker: keyMarker,
|
||||
VersionIdMarker: versionIdMarker,
|
||||
EncodingType: "url",
|
||||
}),
|
||||
{ abortSignal: abortController.signal },
|
||||
)
|
||||
decodeS3UrlEncodedObjectVersions(data)
|
||||
|
||||
const objectsToDelete: { Key: string; VersionId?: string }[] = []
|
||||
data.Versions?.forEach((v) => {
|
||||
@@ -151,9 +154,11 @@ export function createDeleteTaskHelpers(s3Client: S3Client, config: DeleteTaskCo
|
||||
Bucket: bucketName,
|
||||
Prefix: prefix,
|
||||
ContinuationToken: continuationToken,
|
||||
EncodingType: "url",
|
||||
}),
|
||||
{ abortSignal: abortController.signal },
|
||||
)
|
||||
decodeS3UrlEncodedObjectList(data)
|
||||
|
||||
const objectsToDelete = (data.Contents ?? []).filter((item) => item.Key).map((item) => ({ Key: item.Key! }))
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
type S3KeyedItem = {
|
||||
Key?: string
|
||||
}
|
||||
|
||||
type S3PrefixedItem = {
|
||||
Prefix?: string
|
||||
}
|
||||
|
||||
type S3ObjectListResponse = {
|
||||
EncodingType?: string
|
||||
Prefix?: string
|
||||
Delimiter?: string
|
||||
StartAfter?: string
|
||||
Contents?: S3KeyedItem[]
|
||||
CommonPrefixes?: S3PrefixedItem[]
|
||||
}
|
||||
|
||||
type S3ObjectVersionsResponse = {
|
||||
EncodingType?: string
|
||||
Prefix?: string
|
||||
Delimiter?: string
|
||||
KeyMarker?: string
|
||||
NextKeyMarker?: string
|
||||
Versions?: S3KeyedItem[]
|
||||
DeleteMarkers?: S3KeyedItem[]
|
||||
CommonPrefixes?: S3PrefixedItem[]
|
||||
}
|
||||
|
||||
function decodeS3UrlValue(value: string | undefined): string | undefined {
|
||||
if (value == null) return value
|
||||
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeS3UrlEncodedObjectList<T extends S3ObjectListResponse>(response: T): T {
|
||||
if (response.EncodingType !== "url") return response
|
||||
|
||||
response.Prefix = decodeS3UrlValue(response.Prefix)
|
||||
response.Delimiter = decodeS3UrlValue(response.Delimiter)
|
||||
response.StartAfter = decodeS3UrlValue(response.StartAfter)
|
||||
response.Contents?.forEach((item) => {
|
||||
item.Key = decodeS3UrlValue(item.Key)
|
||||
})
|
||||
response.CommonPrefixes?.forEach((item) => {
|
||||
item.Prefix = decodeS3UrlValue(item.Prefix)
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export function decodeS3UrlEncodedObjectVersions<T extends S3ObjectVersionsResponse>(response: T): T {
|
||||
if (response.EncodingType !== "url") return response
|
||||
|
||||
response.Prefix = decodeS3UrlValue(response.Prefix)
|
||||
response.Delimiter = decodeS3UrlValue(response.Delimiter)
|
||||
response.KeyMarker = decodeS3UrlValue(response.KeyMarker)
|
||||
response.NextKeyMarker = decodeS3UrlValue(response.NextKeyMarker)
|
||||
response.Versions?.forEach((item) => {
|
||||
item.Key = decodeS3UrlValue(item.Key)
|
||||
})
|
||||
response.DeleteMarkers?.forEach((item) => {
|
||||
item.Key = decodeS3UrlValue(item.Key)
|
||||
})
|
||||
response.CommonPrefixes?.forEach((item) => {
|
||||
item.Prefix = decodeS3UrlValue(item.Prefix)
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
|
||||
const loadS3ObjectEncoding = () => import(new URL("../../lib/s3-object-encoding.ts", import.meta.url).href)
|
||||
|
||||
test("decodeS3UrlEncodedObjectList restores control-character object keys", async () => {
|
||||
const { decodeS3UrlEncodedObjectList } = await loadS3ObjectEncoding()
|
||||
const response = decodeS3UrlEncodedObjectList({
|
||||
EncodingType: "url",
|
||||
Prefix: "bad%2F",
|
||||
Delimiter: "%2F",
|
||||
StartAfter: "%04bad-prefix%2Fbefore.txt",
|
||||
Contents: [{ Key: "%04bad-prefix%2Fobject.txt" }],
|
||||
CommonPrefixes: [{ Prefix: "normal%2F" }],
|
||||
})
|
||||
|
||||
assert.equal(response.StartAfter, "\x04bad-prefix/before.txt")
|
||||
assert.equal(response.Contents?.[0]?.Key, "\x04bad-prefix/object.txt")
|
||||
assert.equal(response.CommonPrefixes?.[0]?.Prefix, "normal/")
|
||||
assert.equal(response.Prefix, "bad/")
|
||||
assert.equal(response.Delimiter, "/")
|
||||
})
|
||||
|
||||
test("decodeS3UrlEncodedObjectVersions restores encoded key markers", async () => {
|
||||
const { decodeS3UrlEncodedObjectVersions } = await loadS3ObjectEncoding()
|
||||
const response = decodeS3UrlEncodedObjectVersions({
|
||||
EncodingType: "url",
|
||||
Prefix: "%04bad-prefix%2F",
|
||||
KeyMarker: "%04bad-prefix%2Ffrom.txt",
|
||||
NextKeyMarker: "%04bad-prefix%2Fnext.txt",
|
||||
Versions: [{ Key: "%04bad-prefix%2Fobject.txt" }],
|
||||
DeleteMarkers: [{ Key: "%04bad-prefix%2Fdeleted.txt" }],
|
||||
})
|
||||
|
||||
assert.equal(response.Prefix, "\x04bad-prefix/")
|
||||
assert.equal(response.KeyMarker, "\x04bad-prefix/from.txt")
|
||||
assert.equal(response.NextKeyMarker, "\x04bad-prefix/next.txt")
|
||||
assert.equal(response.Versions?.[0]?.Key, "\x04bad-prefix/object.txt")
|
||||
assert.equal(response.DeleteMarkers?.[0]?.Key, "\x04bad-prefix/deleted.txt")
|
||||
})
|
||||
|
||||
test("S3 URL decoding leaves malformed values unchanged", async () => {
|
||||
const { decodeS3UrlEncodedObjectList } = await loadS3ObjectEncoding()
|
||||
const response = decodeS3UrlEncodedObjectList({
|
||||
EncodingType: "url",
|
||||
Contents: [{ Key: "%E0%A4%A" }],
|
||||
})
|
||||
|
||||
assert.equal(response.Contents?.[0]?.Key, "%E0%A4%A")
|
||||
})
|
||||
Reference in New Issue
Block a user