diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx
index 9c61a5e4575..b6ff6d95b07 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ConfirmPurgeModal.tsx
@@ -1,7 +1,7 @@
import { toast } from 'sonner'
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
-import { getPathAlongOpenedFolders } from './StorageExplorer.utils'
+import { getStorageItemPath } from './StorageExplorer.utils'
import { useStorageExplorerNavigation } from './StorageExplorerNavigation'
import { useObjectPurgeMutation } from '@/data/storage/versioning/object-purge-mutation'
import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
@@ -33,11 +33,7 @@ export const ConfirmPurgeModal = () => {
if (!projectRef || !selectedBucket?.id || itemToPurge === undefined) return
// The endpoint addresses an object by full path; a row only knows its leaf name.
- const folderPath = getPathAlongOpenedFolders(
- { openedFolders: openedFolders.slice(0, itemToPurge.columnIndex), selectedBucket },
- false
- )
- const path = [folderPath, itemToPurge.name].filter(Boolean).join('/')
+ const path = getStorageItemPath({ openedFolders }, itemToPurge)
purgeObject({ projectRef, bucketId: selectedBucket.id, path })
}
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx
index a1d11f83d93..7de9d0aec00 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FilePreview.test.tsx
@@ -2,6 +2,7 @@ import { screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { FilePreview } from '@/components/interfaces/Storage/StorageExplorer/FilePreview'
+import { fileUrlKey } from '@/components/interfaces/Storage/StorageExplorer/useFetchFileUrlQuery'
import { customRender as render } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
@@ -65,6 +66,36 @@ describe('FilePreview', () => {
)
})
+ it('keys each version separately, so a new one cannot reuse the cached URL', () => {
+ const current = fileUrlKey({
+ projectRef: 'abcdef',
+ isBucketPublic: false,
+ bucketId: 'my-bucket',
+ path: 'photo.png',
+ })
+
+ expect(
+ fileUrlKey({
+ projectRef: 'abcdef',
+ isBucketPublic: false,
+ bucketId: 'my-bucket',
+ path: 'photo.png',
+ versionId: 'v-new',
+ })
+ ).not.toEqual(current)
+
+ // The version-less key is a prefix of every version's, so one invalidation clears all.
+ expect(
+ fileUrlKey({
+ projectRef: 'abcdef',
+ isBucketPublic: false,
+ bucketId: 'my-bucket',
+ path: 'photo.png',
+ versionId: 'v-new',
+ }).slice(0, current.length)
+ ).toEqual(current)
+ })
+
it('skips the request for a file too large to preview', async () => {
const bodies = mockSignEndpoint()
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx
index 181df2a5022..c225ca069a4 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQuery } from '@tanstack/react-query'
-import { AlertCircle, Archive, ChevronDown, Copy, Download, Trash2, X } from 'lucide-react'
+import { AlertCircle, Archive, ChevronDown, Copy, Download, Trash2, Upload, X } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useRef, useState } from 'react'
import { toast } from 'sonner'
@@ -18,6 +18,7 @@ import { URL_EXPIRY_DURATION } from '../Storage.constants'
import { StorageItem } from '../Storage.types'
import { getBucketVersioningState } from '../StorageVersioning.constants'
import { FilePreview } from './FilePreview'
+import { pickFile } from './pickFile'
import { PreviewSection } from './PreviewSection'
import { getPathAlongOpenedFolders } from './StorageExplorer.utils'
import { useStorageExplorerNavigation } from './StorageExplorerNavigation'
@@ -65,10 +66,13 @@ interface CurrentFilePreviewProps {
size: string | null
isPublicBucket: boolean
isVersionedBucket: boolean
+ /** Pins the preview to the current version, so a new one busts the cached URL. */
+ currentVersionId?: string
hasCurrentVersion: boolean
canUpdateFiles: boolean
onCopyUrl: (path: string, expiry?: number) => void
onDownload: () => void
+ onReplace: () => void
onCustomExpiry: () => void
onDelete: () => void
onPurge: () => void
@@ -81,10 +85,12 @@ const CurrentFilePreview = ({
size,
isPublicBucket,
isVersionedBucket,
+ currentVersionId,
hasCurrentVersion,
canUpdateFiles,
onCopyUrl,
onDownload,
+ onReplace,
onCustomExpiry,
onDelete,
onPurge,
@@ -94,7 +100,12 @@ const CurrentFilePreview = ({
className="flex items-center justify-center overflow-hidden rounded-md border border-overlay"
style={{ height: 'clamp(120px, calc((100vh - 144px) * 0.4), 180px)' }}
>
-
+
@@ -129,6 +140,21 @@ const CurrentFilePreview = ({
tooltip={{ content: { side: 'top', text: 'Download current' } }}
/>
+ {canUpdateFiles && (
+ }
+ onClick={onReplace}
+ tooltip={{
+ content: {
+ side: 'top',
+ text: isVersionedBucket ? 'Upload new version' : 'Replace file',
+ },
+ }}
+ />
+ )}
+
{isPublicBucket ? (