Skip to content

Commit c7a253a

Browse files
committed
feat: enhance removeFile function to support conditional deletion from storage with options
1 parent 5e156ee commit c7a253a

File tree

3 files changed

+98
-9
lines changed

3 files changed

+98
-9
lines changed

docs/content/2.usage/1.overview.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,31 @@ Add multiple files. Returns successfully added files (failed validations are fil
6969
const addedFiles = await uploader.addFiles(Array.from(input.files))
7070
```
7171

72-
#### `removeFile(fileId: string): Promise<void>`
72+
#### `removeFile(fileId: string, options?): Promise<void>`
7373

74-
Remove a file by ID. If the file was uploaded, also removes it from storage (if storage plugin supports it).
74+
Remove a file by ID. Optionally control whether to delete from remote storage.
75+
76+
**Options:**
77+
78+
| Option | Type | Default | Description |
79+
| ------------------- | ------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------------- |
80+
| `deleteFromStorage` | `"always"` \| `"never"` \| `"local-only"` | `"always"` | Controls storage deletion behavior |
81+
82+
**`deleteFromStorage` values:**
83+
84+
- `"always"` - Always delete from storage if the file has a `remoteUrl`
85+
- `"never"` - Never delete from storage, only remove from local state (useful for forms that reference shared files)
86+
- `"local-only"` - Only delete files uploaded in this session (`source === "local"`), preserving files loaded via `initializeExistingFiles`
7587

7688
```ts
89+
// Default: removes from local state AND deletes from storage
7790
await uploader.removeFile(file.id)
91+
92+
// Only remove from local state, keep in storage
93+
await uploader.removeFile(file.id, { deleteFromStorage: "never" })
94+
95+
// Delete from storage only if this form uploaded it
96+
await uploader.removeFile(file.id, { deleteFromStorage: "local-only" })
7897
```
7998

8099
#### `removeFiles(fileIds: string[]): UploadFile[]`

src/runtime/composables/useUploadKit/index.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,19 +414,45 @@ export const useUploadKit = <TUploadResult = any>(_options: UploadOptions = {})
414414
return addedFiles
415415
}
416416

417-
const removeFile = async (fileId: string, removeOptions?: { deleteFromStorage?: boolean }) => {
418-
const { deleteFromStorage = true } = removeOptions ?? {}
417+
/**
418+
* Remove a file from the upload manager.
419+
*
420+
* @param fileId - The ID of the file to remove
421+
* @param removeOptions - Options for controlling storage deletion behavior
422+
* @param removeOptions.deleteFromStorage - Controls whether to delete the file from remote storage:
423+
* - `"always"` (default): Always delete from storage if the file has a remoteUrl
424+
* - `"never"`: Never delete from storage, only remove from local state
425+
* - `"local-only"`: Only delete files that were uploaded in this session (source === "local"),
426+
* preserving files that were loaded from storage via initializeExistingFiles
427+
*/
428+
const removeFile = async (fileId: string, removeOptions?: { deleteFromStorage?: "always" | "never" | "local-only" }) => {
429+
const { deleteFromStorage = "always" } = removeOptions ?? {}
419430
const file = files.value.find((f) => f.id === fileId)
420431

421432
if (!file) return
422433

434+
// Determine if we should delete from storage based on the deleteFromStorage option
435+
let shouldDelete: boolean
436+
switch (deleteFromStorage) {
437+
case "always":
438+
shouldDelete = true
439+
break
440+
case "never":
441+
shouldDelete = false
442+
break
443+
case "local-only":
444+
// Only delete files that were uploaded in this session, not pre-loaded from storage
445+
shouldDelete = file.source === "local"
446+
break
447+
}
448+
423449
// Only call storage plugin's remove hook if:
424-
// - deleteFromStorage is true (default)
450+
// - shouldDelete is true
425451
// - file has a remoteUrl (indicates it exists in remote storage)
426452
// This applies to both:
427453
// - Local files that were uploaded (source: 'local', remoteUrl set after upload)
428454
// - Remote files (source: 'storage' | 'instagram' | etc., remoteUrl set from initialization)
429-
if (deleteFromStorage && file.remoteUrl) {
455+
if (shouldDelete && file.remoteUrl) {
430456
const storagePlugin = getStoragePlugin()
431457
if (storagePlugin?.hooks.remove) {
432458
try {

test/unit/useUploadKit.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ describe("useUploadKit", () => {
182182
expect(uploader.files.value).toHaveLength(1)
183183
})
184184

185-
it("should call storage plugin remove hook by default for files with remoteUrl", async () => {
185+
it("should call storage plugin remove hook by default (deleteFromStorage: 'always')", async () => {
186186
const removeHook = vi.fn()
187187
const storagePlugin: StoragePlugin = {
188188
id: "test-storage",
@@ -212,7 +212,7 @@ describe("useUploadKit", () => {
212212
expect(uploader.files.value).toHaveLength(0)
213213
})
214214

215-
it("should skip storage plugin remove hook when deleteFromStorage is false", async () => {
215+
it("should skip storage plugin remove hook when deleteFromStorage is 'never'", async () => {
216216
const removeHook = vi.fn()
217217
const storagePlugin: StoragePlugin = {
218218
id: "test-storage",
@@ -232,12 +232,55 @@ describe("useUploadKit", () => {
232232
// Add a remote file using initializeExistingFiles
233233
await uploader.initializeExistingFiles([{ id: "remote-1" }])
234234

235-
await uploader.removeFile("remote-1", { deleteFromStorage: false })
235+
await uploader.removeFile("remote-1", { deleteFromStorage: "never" })
236236

237237
expect(removeHook).not.toHaveBeenCalled()
238238
expect(uploader.files.value).toHaveLength(0)
239239
})
240240

241+
it("should only delete local uploads when deleteFromStorage is 'local-only'", async () => {
242+
const removeHook = vi.fn()
243+
const uploadHook = vi.fn().mockResolvedValue({
244+
url: "https://storage.example.com/uploaded.jpg",
245+
storageKey: "uploaded.jpg",
246+
})
247+
const storagePlugin: StoragePlugin = {
248+
id: "test-storage",
249+
hooks: {
250+
upload: uploadHook,
251+
remove: removeHook,
252+
getRemoteFile: vi.fn().mockResolvedValue({
253+
size: 1024,
254+
mimeType: "image/jpeg",
255+
remoteUrl: "https://storage.example.com/existing.jpg",
256+
}),
257+
},
258+
}
259+
260+
const uploader = useUploadKit({ storage: storagePlugin })
261+
262+
// Add a remote file (source: "storage") - simulates pre-populated file
263+
await uploader.initializeExistingFiles([{ id: "existing-file" }])
264+
265+
// Add and upload a local file (source: "local")
266+
await uploader.addFile(createMockFile("new-upload.jpg"))
267+
const localFileId = uploader.files.value.find((f) => f.source === "local")!.id
268+
269+
// Upload to set remoteUrl
270+
await uploader.upload()
271+
272+
expect(uploader.files.value).toHaveLength(2)
273+
274+
// Remove the pre-populated file with "local-only" - should NOT delete from storage
275+
await uploader.removeFile("existing-file", { deleteFromStorage: "local-only" })
276+
expect(removeHook).not.toHaveBeenCalled()
277+
278+
// Remove the locally uploaded file with "local-only" - SHOULD delete from storage
279+
await uploader.removeFile(localFileId, { deleteFromStorage: "local-only" })
280+
expect(removeHook).toHaveBeenCalledTimes(1)
281+
expect(removeHook).toHaveBeenCalledWith(expect.objectContaining({ source: "local" }), expect.any(Object))
282+
})
283+
241284
it("should not call storage plugin remove hook for files without remoteUrl", async () => {
242285
const removeHook = vi.fn()
243286
const storagePlugin: StoragePlugin = {
@@ -252,6 +295,7 @@ describe("useUploadKit", () => {
252295
await uploader.addFile(createMockFile("local.jpg"))
253296
const fileId = uploader.files.value[0]!.id
254297

298+
// File hasn't been uploaded yet, so no remoteUrl
255299
await uploader.removeFile(fileId)
256300

257301
// Should not call remove hook since file doesn't have remoteUrl yet

0 commit comments

Comments
 (0)