mirror of
https://github.com/rustfs/console.git
synced 2026-10-29 18:44:56 +08:00
fix(lifecycle): preserve expiration days with marker cleanup (#170)
This commit is contained in:
@@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next"
|
||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
@@ -16,7 +16,15 @@ import { useTiers, type TierRow } from "@/hooks/use-tiers"
|
||||
import { useMessage } from "@/lib/feedback/message"
|
||||
import { randomUUID } from "@/lib/functions"
|
||||
import { isMissingBucketConfiguration } from "@/lib/bucket-configuration"
|
||||
import { buildCurrentVersionExpiration, buildLifecycleFilter } from "@/lib/bucket-lifecycle"
|
||||
import {
|
||||
buildCurrentVersionExpirationRules,
|
||||
buildLifecycleFilter,
|
||||
findIncompleteLifecycleTag,
|
||||
getBucketVersioningMode,
|
||||
hasCompleteLifecycleTags,
|
||||
MAX_LIFECYCLE_RULES,
|
||||
type BucketVersioningMode,
|
||||
} from "@/lib/bucket-lifecycle"
|
||||
|
||||
interface LifecycleNewFormProps {
|
||||
open: boolean
|
||||
@@ -47,13 +55,20 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
const [tiersLoading, setTiersLoading] = useState(false)
|
||||
const [tiersError, setTiersError] = useState("")
|
||||
const [tiersReloadVersion, setTiersReloadVersion] = useState(0)
|
||||
const [versioningStatus, setVersioningStatus] = useState(false)
|
||||
const [versioningMode, setVersioningMode] = useState<BucketVersioningMode>("unversioned")
|
||||
const [versioningLoading, setVersioningLoading] = useState(false)
|
||||
const [versioningError, setVersioningError] = useState("")
|
||||
const [versioningReloadVersion, setVersioningReloadVersion] = useState(0)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [saveError, setSaveError] = useState("")
|
||||
const [fieldErrors, setFieldErrors] = useState<{ days?: string; storageType?: string }>({})
|
||||
const [fieldErrors, setFieldErrors] = useState<{
|
||||
days?: string
|
||||
storageType?: string
|
||||
tags?: string
|
||||
deleteMarker?: string
|
||||
}>({})
|
||||
|
||||
const hasVersionHistory = versioningMode !== "unversioned"
|
||||
|
||||
const versionOptions = useMemo(
|
||||
() => [
|
||||
@@ -89,16 +104,16 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
|
||||
const loadVersioningStatus = useCallback(async () => {
|
||||
if (!bucketName) {
|
||||
setVersioningStatus(false)
|
||||
setVersioningMode("unversioned")
|
||||
return
|
||||
}
|
||||
setVersioningLoading(true)
|
||||
setVersioningError("")
|
||||
try {
|
||||
const resp = await getBucketVersioning(bucketName)
|
||||
setVersioningStatus(resp.Status === "Enabled")
|
||||
setVersioningMode(getBucketVersioningMode(resp.Status))
|
||||
} catch {
|
||||
setVersioningStatus(false)
|
||||
setVersioningMode("unversioned")
|
||||
setVersioningError(t("Unable to load versioning status."))
|
||||
} finally {
|
||||
setVersioningLoading(false)
|
||||
@@ -113,8 +128,15 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
}, [open, loadTiers, loadVersioningStatus, tiersReloadVersion, versioningReloadVersion])
|
||||
|
||||
useEffect(() => {
|
||||
if (versionType !== "current") setExpiredDeleteMark(false)
|
||||
}, [versionType])
|
||||
if (activeTab !== "expire" || versionType !== "current") setExpiredDeleteMark(false)
|
||||
}, [activeTab, versionType])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasVersionHistory) {
|
||||
setVersionType("current")
|
||||
setExpiredDeleteMark(false)
|
||||
}
|
||||
}, [hasVersionHistory])
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setActiveTab("expire")
|
||||
@@ -141,20 +163,31 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
const removeTag = (index: number) => {
|
||||
if (tags.length === 1) return
|
||||
setTags((prev) => prev.filter((_, i) => i !== index))
|
||||
setFieldErrors((current) => ({ ...current, tags: undefined, deleteMarker: undefined }))
|
||||
}
|
||||
|
||||
const updateTag = (index: number, field: "key" | "value", value: string) => {
|
||||
setTags((prev) => prev.map((tag, i) => (i === index ? { ...tag, [field]: value } : tag)))
|
||||
setFieldErrors((current) => ({ ...current, tags: undefined, deleteMarker: undefined }))
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
const errors: { days?: string; storageType?: string } = {}
|
||||
const errors: typeof fieldErrors = {}
|
||||
if (!days || days < 1) {
|
||||
errors.days = t("Please enter valid days")
|
||||
}
|
||||
if (activeTab === "transition" && !storageType) {
|
||||
errors.storageType = t("Please select storage type")
|
||||
}
|
||||
|
||||
const incompleteTagIndex = findIncompleteLifecycleTag(tags)
|
||||
if (incompleteTagIndex >= 0) {
|
||||
errors.tags = tags[incompleteTagIndex].key.trim() ? t("Please enter tag value") : t("Please enter key name")
|
||||
}
|
||||
if (activeTab === "expire" && versionType === "current" && expiredDeleteMark && hasCompleteLifecycleTags(tags)) {
|
||||
errors.deleteMarker = `${t("Delete Marker Handling")}: ${t("Tags")} ${t("Unsupported")}`
|
||||
}
|
||||
|
||||
setFieldErrors(errors)
|
||||
const firstErrorId = errors.days
|
||||
? activeTab === "transition"
|
||||
@@ -162,7 +195,13 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
: "lifecycle-expire-days"
|
||||
: errors.storageType
|
||||
? "lifecycle-storage-type"
|
||||
: null
|
||||
: errors.tags && incompleteTagIndex >= 0
|
||||
? tags[incompleteTagIndex].key.trim()
|
||||
? `lifecycle-${activeTab}-tag-value-${incompleteTagIndex}`
|
||||
: `lifecycle-${activeTab}-tag-key-${incompleteTagIndex}`
|
||||
: errors.deleteMarker
|
||||
? "lifecycle-expired-delete-marker"
|
||||
: null
|
||||
if (firstErrorId) document.getElementById(firstErrorId)?.focus()
|
||||
return !firstErrorId
|
||||
}
|
||||
@@ -189,30 +228,38 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
}
|
||||
}
|
||||
|
||||
const newRule: Record<string, unknown> = {
|
||||
ID: randomUUID(),
|
||||
const filter = buildLifecycleFilter(prefix, tags)
|
||||
const baseId = randomUUID()
|
||||
const baseRule: Record<string, unknown> = {
|
||||
ID: baseId,
|
||||
Status: "Enabled",
|
||||
Filter: buildLifecycleFilter(prefix, tags),
|
||||
Filter: filter,
|
||||
}
|
||||
|
||||
const daysValue = Number(days)
|
||||
let newRules: Record<string, unknown>[]
|
||||
|
||||
if (activeTab === "expire") {
|
||||
if (versionType === "non-current") {
|
||||
newRule.NoncurrentVersionExpiration = { NoncurrentDays: daysValue }
|
||||
baseRule.NoncurrentVersionExpiration = { NoncurrentDays: daysValue }
|
||||
newRules = [baseRule]
|
||||
} else {
|
||||
newRule.Expiration = buildCurrentVersionExpiration(daysValue, expiredDeleteMark)
|
||||
newRules = buildCurrentVersionExpirationRules(baseId, daysValue, filter, expiredDeleteMark)
|
||||
}
|
||||
} else {
|
||||
if (versionType === "non-current") {
|
||||
newRule.NoncurrentVersionTransitions = [{ NoncurrentDays: daysValue, StorageClass: storageType }]
|
||||
baseRule.NoncurrentVersionTransitions = [{ NoncurrentDays: daysValue, StorageClass: storageType }]
|
||||
} else {
|
||||
newRule.Transitions = [{ Days: daysValue, StorageClass: storageType }]
|
||||
baseRule.Transitions = [{ Days: daysValue, StorageClass: storageType }]
|
||||
}
|
||||
newRules = [baseRule]
|
||||
}
|
||||
|
||||
const existingRules = currentConfig?.Rules ?? []
|
||||
const payload = { Rules: [...existingRules, newRule] }
|
||||
if (existingRules.length + newRules.length > MAX_LIFECYCLE_RULES) {
|
||||
throw new Error(t("Lifecycle configuration cannot contain more than 1000 rules."))
|
||||
}
|
||||
const payload = { Rules: [...existingRules, ...newRules] }
|
||||
|
||||
await putBucketLifecycleConfiguration(bucketName, payload)
|
||||
message.success(t("Create Success"))
|
||||
@@ -309,7 +356,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
|
||||
<TabsContent value="expire" className="mt-0 space-y-6">
|
||||
<div className="space-y-4">
|
||||
{versioningStatus && (
|
||||
{hasVersionHistory && (
|
||||
<Field>
|
||||
<FieldLabel>{t("Object Version")}</FieldLabel>
|
||||
<FieldContent>
|
||||
@@ -378,7 +425,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3" aria-invalid={Boolean(fieldErrors.tags)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel className="text-sm font-medium">{t("Tags")}</FieldLabel>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addTag} disabled={submitting}>
|
||||
@@ -396,6 +443,10 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
value={tag.key}
|
||||
onChange={(e) => updateTag(index, "key", e.target.value)}
|
||||
aria-label={`${t("Tag Name")} ${index + 1}`}
|
||||
aria-invalid={
|
||||
Boolean(fieldErrors.tags) && Boolean(tag.key.trim()) !== Boolean(tag.value.trim())
|
||||
}
|
||||
aria-describedby={fieldErrors.tags ? "lifecycle-expire-tags-error" : undefined}
|
||||
autoComplete="off"
|
||||
placeholder={t("Tag Name")}
|
||||
spellCheck={false}
|
||||
@@ -407,6 +458,10 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
value={tag.value}
|
||||
onChange={(e) => updateTag(index, "value", e.target.value)}
|
||||
aria-label={`${t("Tag Value")} ${index + 1}`}
|
||||
aria-invalid={
|
||||
Boolean(fieldErrors.tags) && Boolean(tag.key.trim()) !== Boolean(tag.value.trim())
|
||||
}
|
||||
aria-describedby={fieldErrors.tags ? "lifecycle-expire-tags-error" : undefined}
|
||||
autoComplete="off"
|
||||
placeholder={t("Tag Value")}
|
||||
className="flex-1"
|
||||
@@ -428,39 +483,68 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<FieldError id="lifecycle-expire-tags-error">{fieldErrors.tags}</FieldError>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{versionType === "current" && (
|
||||
{hasVersionHistory && versionType === "current" && (
|
||||
<details>
|
||||
<summary className="cursor-pointer text-sm font-medium text-primary">
|
||||
{t("Advanced Settings")}
|
||||
</summary>
|
||||
<Field className="mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<FieldLabel htmlFor="lifecycle-expired-delete-marker" className="text-sm font-medium">
|
||||
{t("Delete Marker Handling")}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
<label
|
||||
htmlFor="lifecycle-expired-delete-marker"
|
||||
className="flex cursor-pointer items-start gap-3"
|
||||
>
|
||||
<Checkbox
|
||||
id="lifecycle-expired-delete-marker"
|
||||
name="lifecycle-expired-delete-marker"
|
||||
checked={expiredDeleteMark}
|
||||
aria-invalid={Boolean(fieldErrors.deleteMarker)}
|
||||
aria-describedby={
|
||||
fieldErrors.deleteMarker
|
||||
? "lifecycle-expired-delete-marker-description lifecycle-expired-delete-marker-error"
|
||||
: "lifecycle-expired-delete-marker-description"
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
setExpiredDeleteMark(checked === true)
|
||||
setFieldErrors((current) => ({ ...current, deleteMarker: undefined }))
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<span>
|
||||
<span className="block text-sm font-medium">{t("Delete Marker Handling")}</span>
|
||||
<FieldDescription id="lifecycle-expired-delete-marker-description">
|
||||
{t("If no versions remain, delete references to this object")}
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<Switch
|
||||
id="lifecycle-expired-delete-marker"
|
||||
checked={expiredDeleteMark}
|
||||
onCheckedChange={setExpiredDeleteMark}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
<FieldError id="lifecycle-expired-delete-marker-error">{fieldErrors.deleteMarker}</FieldError>
|
||||
</Field>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{hasVersionHistory && versionType === "current" ? (
|
||||
<Alert>
|
||||
<AlertTitle>{t("Warning")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.",
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{activeTab === "expire" && expiredDeleteMark ? (
|
||||
<p className="text-sm text-muted-foreground">{t("2 lifecycle rules will be created.")}</p>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="transition" className="mt-0 space-y-6">
|
||||
<div className="space-y-4">
|
||||
{versioningStatus && (
|
||||
{hasVersionHistory && (
|
||||
<Field>
|
||||
<FieldLabel>{t("Object Version")}</FieldLabel>
|
||||
<FieldContent>
|
||||
@@ -561,7 +645,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
</FieldContent>
|
||||
</Field>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3" aria-invalid={Boolean(fieldErrors.tags)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel className="text-sm font-medium">{t("Tags")}</FieldLabel>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addTag} disabled={submitting}>
|
||||
@@ -579,6 +663,10 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
value={tag.key}
|
||||
onChange={(e) => updateTag(index, "key", e.target.value)}
|
||||
aria-label={`${t("Tag Name")} ${index + 1}`}
|
||||
aria-invalid={
|
||||
Boolean(fieldErrors.tags) && Boolean(tag.key.trim()) !== Boolean(tag.value.trim())
|
||||
}
|
||||
aria-describedby={fieldErrors.tags ? "lifecycle-transition-tags-error" : undefined}
|
||||
autoComplete="off"
|
||||
placeholder={t("Tag Name")}
|
||||
spellCheck={false}
|
||||
@@ -590,6 +678,10 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
value={tag.value}
|
||||
onChange={(e) => updateTag(index, "value", e.target.value)}
|
||||
aria-label={`${t("Tag Value")} ${index + 1}`}
|
||||
aria-invalid={
|
||||
Boolean(fieldErrors.tags) && Boolean(tag.key.trim()) !== Boolean(tag.value.trim())
|
||||
}
|
||||
aria-describedby={fieldErrors.tags ? "lifecycle-transition-tags-error" : undefined}
|
||||
autoComplete="off"
|
||||
placeholder={t("Tag Value")}
|
||||
className="flex-1"
|
||||
@@ -611,6 +703,7 @@ export function LifecycleNewForm({ open, onOpenChange, bucketName, onSuccess }:
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<FieldError id="lifecycle-transition-tags-error">{fieldErrors.tags}</FieldError>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "يؤدي انتهاء صلاحية الإصدار الحالي إلى إنشاء علامة حذف. قم بتكوين انتهاء صلاحية الإصدارات غير الحالية لإزالة بيانات الكائن نهائيًا.",
|
||||
"2 lifecycle rules will be created.": "سيتم إنشاء قاعدتين لدورة الحياة.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "لا يمكن أن يحتوي تكوين دورة الحياة على أكثر من 1000 قاعدة."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "Beim Ablauf der aktuellen Version wird eine Löschmarkierung erstellt. Konfigurieren Sie den Ablauf nicht aktueller Versionen, um Objektdaten dauerhaft zu entfernen.",
|
||||
"2 lifecycle rules will be created.": "Es werden 2 Lebenszyklusregeln erstellt.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "Die Lebenszykluskonfiguration darf nicht mehr als 1000 Regeln enthalten."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.",
|
||||
"2 lifecycle rules will be created.": "2 lifecycle rules will be created.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "Lifecycle configuration cannot contain more than 1000 rules."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "La expiración de la versión actual crea un marcador de eliminación. Configura la expiración de versiones no actuales para eliminar permanentemente los datos del objeto.",
|
||||
"2 lifecycle rules will be created.": "Se crearán 2 reglas de ciclo de vida.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "La configuración del ciclo de vida no puede contener más de 1000 reglas."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "L’expiration de la version actuelle crée un marqueur de suppression. Configurez l’expiration des versions non actuelles pour supprimer définitivement les données de l’objet.",
|
||||
"2 lifecycle rules will be created.": "2 règles de cycle de vie seront créées.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "La configuration du cycle de vie ne peut pas contenir plus de 1 000 règles."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "Kedaluwarsa versi saat ini membuat penanda penghapusan. Konfigurasikan kedaluwarsa versi nonaktif untuk menghapus data objek secara permanen.",
|
||||
"2 lifecycle rules will be created.": "2 aturan siklus hidup akan dibuat.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "Konfigurasi siklus hidup tidak boleh berisi lebih dari 1000 aturan."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "La scadenza della versione corrente crea un indicatore di eliminazione. Configura la scadenza delle versioni non correnti per rimuovere definitivamente i dati dell’oggetto.",
|
||||
"2 lifecycle rules will be created.": "Verranno create 2 regole del ciclo di vita.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "La configurazione del ciclo di vita non può contenere più di 1000 regole."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "現行バージョンの有効期限切れでは削除マーカーが作成されます。オブジェクトデータを完全に削除するには、非現行バージョンの有効期限を設定してください。",
|
||||
"2 lifecycle rules will be created.": "2 件のライフサイクルルールが作成されます。",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "ライフサイクル設定に含められるルールは 1000 件までです。"
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "현재 버전 만료 시 삭제 마커가 생성됩니다. 객체 데이터를 영구적으로 삭제하려면 비현재 버전 만료를 구성하세요.",
|
||||
"2 lifecycle rules will be created.": "수명 주기 규칙 2개가 생성됩니다.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "수명 주기 구성에는 1000개를 초과하는 규칙을 포함할 수 없습니다."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "A expiração da versão atual cria um marcador de exclusão. Configure a expiração de versões não atuais para remover permanentemente os dados do objeto.",
|
||||
"2 lifecycle rules will be created.": "Serão criadas 2 regras de ciclo de vida.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "A configuração do ciclo de vida não pode conter mais de 1000 regras."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Загрузить больше объектов",
|
||||
"Scroll to load more objects": "Прокрутить для загрузки объектов",
|
||||
"Back to top": "Перейти вверх",
|
||||
"Go to bottom": "Перейти вниз"
|
||||
"Go to bottom": "Перейти вниз",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "При истечении срока действия текущей версии создается маркер удаления. Настройте удаление неактуальных версий, чтобы окончательно удалить данные объекта.",
|
||||
"2 lifecycle rules will be created.": "Будут созданы 2 правила жизненного цикла.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "Конфигурация жизненного цикла не может содержать более 1000 правил."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "Geçerli sürümün süresi dolduğunda bir silme işareti oluşturulur. Nesne verilerini kalıcı olarak kaldırmak için geçerli olmayan sürümlerin süre sonunu yapılandırın.",
|
||||
"2 lifecycle rules will be created.": "2 yaşam döngüsü kuralı oluşturulacak.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "Yaşam döngüsü yapılandırması 1000'den fazla kural içeremez."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "Loading more objects",
|
||||
"Scroll to load more objects": "Scroll to load more objects",
|
||||
"Back to top": "Back to top",
|
||||
"Go to bottom": "Go to bottom"
|
||||
"Go to bottom": "Go to bottom",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "Việc hết hạn phiên bản hiện tại sẽ tạo dấu xóa. Hãy cấu hình hết hạn phiên bản không hiện tại để xóa vĩnh viễn dữ liệu đối tượng.",
|
||||
"2 lifecycle rules will be created.": "Sẽ tạo 2 quy tắc vòng đời.",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "Cấu hình vòng đời không được chứa quá 1000 quy tắc."
|
||||
}
|
||||
|
||||
@@ -1501,5 +1501,8 @@
|
||||
"Loading more objects": "正在加载更多对象",
|
||||
"Scroll to load more objects": "滚动以加载更多对象",
|
||||
"Back to top": "回到顶部",
|
||||
"Go to bottom": "直达底部"
|
||||
"Go to bottom": "直达底部",
|
||||
"Current version expiration creates a delete marker. Configure non-current version expiration to permanently remove object data.": "当前版本过期时会创建删除标记。请配置非当前版本过期规则,以永久删除对象数据。",
|
||||
"2 lifecycle rules will be created.": "将创建 2 条生命周期规则。",
|
||||
"Lifecycle configuration cannot contain more than 1000 rules.": "生命周期配置不能包含超过 1000 条规则。"
|
||||
}
|
||||
|
||||
+44
-3
@@ -3,8 +3,26 @@ export interface LifecycleFilterTag {
|
||||
value: string
|
||||
}
|
||||
|
||||
export type BucketVersioningMode = "unversioned" | "enabled" | "suspended"
|
||||
|
||||
export const MAX_LIFECYCLE_RULES = 1000
|
||||
|
||||
export function getBucketVersioningMode(status?: string): BucketVersioningMode {
|
||||
if (status === "Enabled") return "enabled"
|
||||
if (status === "Suspended") return "suspended"
|
||||
return "unversioned"
|
||||
}
|
||||
|
||||
export function findIncompleteLifecycleTag(tags: LifecycleFilterTag[]): number {
|
||||
return tags.findIndex((tag) => Boolean(tag.key.trim()) !== Boolean(tag.value.trim()))
|
||||
}
|
||||
|
||||
export function hasCompleteLifecycleTags(tags: LifecycleFilterTag[]): boolean {
|
||||
return tags.some((tag) => Boolean(tag.key.trim()) && Boolean(tag.value.trim()))
|
||||
}
|
||||
|
||||
export function buildLifecycleFilter(prefix: string, tags: LifecycleFilterTag[]): Record<string, unknown> {
|
||||
const validTags = tags.filter((tag) => tag.key && tag.value)
|
||||
const validTags = tags.filter((tag) => tag.key.trim() && tag.value.trim())
|
||||
if (!prefix && validTags.length === 0) return { Prefix: "" }
|
||||
|
||||
const [firstTag] = validTags
|
||||
@@ -24,6 +42,29 @@ export function buildLifecycleFilter(prefix: string, tags: LifecycleFilterTag[])
|
||||
return { Prefix: prefix }
|
||||
}
|
||||
|
||||
export function buildCurrentVersionExpiration(days: number, expiredDeleteMarker: boolean): Record<string, unknown> {
|
||||
return expiredDeleteMarker ? { ExpiredObjectDeleteMarker: true } : { Days: days }
|
||||
export function buildCurrentVersionExpirationRules(
|
||||
baseId: string,
|
||||
days: number,
|
||||
filter: Record<string, unknown>,
|
||||
cleanupExpiredDeleteMarkers: boolean,
|
||||
): Record<string, unknown>[] {
|
||||
const rules: Record<string, unknown>[] = [
|
||||
{
|
||||
ID: `${baseId}-expiration`,
|
||||
Status: "Enabled",
|
||||
Filter: filter,
|
||||
Expiration: { Days: days },
|
||||
},
|
||||
]
|
||||
|
||||
if (cleanupExpiredDeleteMarkers) {
|
||||
rules.push({
|
||||
ID: `${baseId}-delete-marker`,
|
||||
Status: "Enabled",
|
||||
Filter: filter,
|
||||
Expiration: { ExpiredObjectDeleteMarker: true },
|
||||
})
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { PutBucketLifecycleConfigurationCommand, S3Client } from "@aws-sdk/client-s3"
|
||||
import {
|
||||
createLatestRequestGate,
|
||||
isMissingBucketConfiguration,
|
||||
normalizeReplicationRulesForRolelessConfig,
|
||||
removeMatchingBucketRule,
|
||||
} from "../../lib/bucket-configuration.ts"
|
||||
import { buildCurrentVersionExpiration, buildLifecycleFilter } from "../../lib/bucket-lifecycle.ts"
|
||||
import {
|
||||
buildCurrentVersionExpirationRules,
|
||||
buildLifecycleFilter,
|
||||
findIncompleteLifecycleTag,
|
||||
getBucketVersioningMode,
|
||||
hasCompleteLifecycleTags,
|
||||
MAX_LIFECYCLE_RULES,
|
||||
} from "../../lib/bucket-lifecycle.ts"
|
||||
import { getXmlErrorCode } from "../../lib/error-handler.ts"
|
||||
|
||||
const bucketInfoSource = await readFile(new URL("../../components/buckets/info.tsx", import.meta.url), "utf8")
|
||||
@@ -84,11 +92,86 @@ test("latest request gate invalidates stale and unmounted responses", () => {
|
||||
assert.equal(gate.isCurrent(second), false)
|
||||
})
|
||||
|
||||
test("lifecycle payload helpers always emit a valid all-bucket filter and exclusive expiration fields", () => {
|
||||
test("lifecycle payload helpers keep current expiration and delete-marker cleanup in separate rules", () => {
|
||||
assert.deepEqual(buildLifecycleFilter("", [{ key: "", value: "" }]), { Prefix: "" })
|
||||
assert.deepEqual(buildLifecycleFilter("logs/", []), { Prefix: "logs/" })
|
||||
assert.deepEqual(buildCurrentVersionExpiration(30, false), { Days: 30 })
|
||||
assert.deepEqual(buildCurrentVersionExpiration(30, true), { ExpiredObjectDeleteMarker: true })
|
||||
|
||||
const filter = { Prefix: "logs/" }
|
||||
assert.deepEqual(buildCurrentVersionExpirationRules("rule", 30, filter, false), [
|
||||
{
|
||||
ID: "rule-expiration",
|
||||
Status: "Enabled",
|
||||
Filter: filter,
|
||||
Expiration: { Days: 30 },
|
||||
},
|
||||
])
|
||||
assert.deepEqual(buildCurrentVersionExpirationRules("rule", 30, filter, true), [
|
||||
{
|
||||
ID: "rule-expiration",
|
||||
Status: "Enabled",
|
||||
Filter: filter,
|
||||
Expiration: { Days: 30 },
|
||||
},
|
||||
{
|
||||
ID: "rule-delete-marker",
|
||||
Status: "Enabled",
|
||||
Filter: filter,
|
||||
Expiration: { ExpiredObjectDeleteMarker: true },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("AWS SDK serializes expiration days and delete-marker cleanup as two valid rules", async () => {
|
||||
let requestBody = ""
|
||||
const client = new S3Client({
|
||||
region: "us-east-1",
|
||||
endpoint: "http://127.0.0.1:9000",
|
||||
credentials: { accessKeyId: "test", secretAccessKey: "test" },
|
||||
requestHandler: {
|
||||
async handle(request) {
|
||||
requestBody = typeof request.body === "string" ? request.body : new TextDecoder().decode(request.body)
|
||||
return { response: { statusCode: 200, headers: {}, body: new Uint8Array() } }
|
||||
},
|
||||
},
|
||||
})
|
||||
const filter = { Prefix: "logs/" }
|
||||
|
||||
await client.send(
|
||||
new PutBucketLifecycleConfigurationCommand({
|
||||
Bucket: "test-bucket",
|
||||
LifecycleConfiguration: {
|
||||
Rules: buildCurrentVersionExpirationRules("rule", 30, filter, true),
|
||||
},
|
||||
}),
|
||||
)
|
||||
client.destroy()
|
||||
|
||||
const expirations = [...requestBody.matchAll(/<Expiration>(.*?)<\/Expiration>/g)].map((match) => match[1])
|
||||
assert.equal((requestBody.match(/<Rule>/g) ?? []).length, 2)
|
||||
assert.deepEqual(expirations, ["<Days>30</Days>", "<ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker>"])
|
||||
})
|
||||
|
||||
test("lifecycle helpers preserve suspended versioning and reject partial tag pairs", () => {
|
||||
assert.equal(MAX_LIFECYCLE_RULES, 1000)
|
||||
assert.equal(getBucketVersioningMode("Enabled"), "enabled")
|
||||
assert.equal(getBucketVersioningMode("Suspended"), "suspended")
|
||||
assert.equal(getBucketVersioningMode(undefined), "unversioned")
|
||||
|
||||
assert.equal(findIncompleteLifecycleTag([{ key: "", value: "" }]), -1)
|
||||
assert.equal(findIncompleteLifecycleTag([{ key: "env", value: "" }]), 0)
|
||||
assert.equal(findIncompleteLifecycleTag([{ key: "", value: "prod" }]), 0)
|
||||
assert.equal(hasCompleteLifecycleTags([{ key: "env", value: "prod" }]), true)
|
||||
assert.equal(hasCompleteLifecycleTags([{ key: "", value: "" }]), false)
|
||||
|
||||
assert.match(
|
||||
lifecycleFormSource,
|
||||
/buildCurrentVersionExpirationRules\(baseId, daysValue, filter, expiredDeleteMark\)/,
|
||||
)
|
||||
assert.match(lifecycleFormSource, /Rules: \[\.\.\.existingRules, \.\.\.newRules\]/)
|
||||
assert.match(lifecycleFormSource, /existingRules\.length \+ newRules\.length > MAX_LIFECYCLE_RULES/)
|
||||
assert.match(lifecycleFormSource, /const hasVersionHistory = versioningMode !== "unversioned"/)
|
||||
assert.match(lifecycleFormSource, /expiredDeleteMark && hasCompleteLifecycleTags\(tags\)/)
|
||||
assert.match(lifecycleFormSource, /setExpiredDeleteMark\(checked === true\)/)
|
||||
})
|
||||
|
||||
test("bucket reads fail closed and stale responses cannot update the active bucket", () => {
|
||||
|
||||
Reference in New Issue
Block a user