Skip to content

Conversation

@osama-rizk
Copy link
Contributor

Description of changes

This PR adds folder deletion support to the Storage remove API.

Key Features:

  • Folder Deletion: Enhanced remove API to delete entire folders and their contents using S3 batch operations
  • Progress Tracking: Added progress callbacks for batch deletion operations with success/failure reporting
  • Cancellation Support: Integrated CancellationToken for long-running folder deletion operations
  • Path Validation: Added safety checks to prevent deletion of dangerous paths (root, bucket-wide)

Technical Implementation:

  • New deleteObjects S3 client operation for batch deletion
  • deleteFolderContents utility for recursive folder deletion with pagination
  • isPathFolder utility to detect folders vs files
  • Function overloads on remove APIs for proper type inference
  • Comprehensive test coverage for all new functionality

Issue #, if available

Closes #5841

Description of how you validated changes

  • Unit Tests: Added test coverage for all utilities and API changes
  • Manual Testing: Linked the JS package locally with UI (storage browser) and verify the backward compatibility and the new functionality (folder deletion, progress tracking, cancelling......etc)

Checklist

  • PR description included
  • yarn test passes
  • Unit Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)

Checklist for repo maintainers

  • Verify E2E tests for existing workflows are working as expected or add E2E tests for newly added workflows
  • New source file paths included in this PR have been added to CODEOWNERS, if appropriate

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@osama-rizk osama-rizk added the run-tests run the pr-label workflow label Jan 12, 2026
@osama-rizk osama-rizk requested a review from a team as a code owner January 12, 2026 16:10
@osama-rizk osama-rizk added run-tests run the pr-label workflow and removed run-tests run the pr-label workflow labels Jan 12, 2026
@soberm soberm self-assigned this Jan 13, 2026
@osama-rizk osama-rizk force-pushed the feature/storage-remove-folder-deletion branch from 4e2d753 to 1849103 Compare January 13, 2026 12:03
@soberm
Copy link
Contributor

soberm commented Jan 13, 2026

Did you also create E2E tests?

@osama-rizk
Copy link
Contributor Author

osama-rizk commented Jan 13, 2026

Did you also create E2E tests?

Not yet. But will do in a follow up PR. BTW the existing e2e test already verifying this change is working (for files) and backward compatible.

soberm
soberm previously approved these changes Jan 13, 2026
delete: '',
}).toString();

const objects = input
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better use generateDeleteObjectsXml helper function

}).toString();

const objects = input
.Delete!.Objects?.map(obj => `<Object><Key>${obj.Key}</Key></Object>`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objects is not optional. the ? is not needed here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also (the optional) VersionId is missing here

export interface DeleteObjectsCommandInput {
Bucket: string;
Delete: {
Objects: Array<{ Key: string; VersionId?: string }>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternative to what I mentioned further up, remove the VersionId from the types

Quiet: false,
},
ExpectedBucketOwner: expectedBucketOwner,
ContentMD5: await calculateContentMd5(xmlBody),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this to the serializer

}

const batch = listResult.Contents.map(obj => ({ Key: obj.Key! }));
const xmlBody = generateDeleteObjectsXml(batch, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be done in the serializer

* Represents an ongoing remove operation with cancellation and state tracking capabilities
* @template T - The type of the result (RemoveWithPathOutput | RemoveOutput)
*/
export interface RemoveOperation<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better update this type to

interface NonPausableTransferTask<T>
  extends Omit<TransferTask<T>, 'pause' | 'resume' | 'state'> {
  state: Omit<TransferTask<T>['state'], 'PAUSED'>;
}

interface RemoveOperation<T> extends NonPausableTransferTask<T>, Promise<T> {}

);
then: wrappedPromise.then.bind(wrappedPromise),
catch: wrappedPromise.catch.bind(wrappedPromise),
finally: wrappedPromise.finally.bind(wrappedPromise),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use Object.assign here, to assign these operation props to the wrappedPromise and then you can drop the then/catch/finally

logger.debug(`removing object in path "${finalKey}"`);
}
): RemoveOperation<RemoveOutput | RemoveWithPathOutput> {
const cancellationToken = new CancellationToken();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here an AbortController is better to use as it is built-in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ultimately check if the AbortController can be passed all the way down to the transferHandler/fetchHandler.

let state: RemoveTaskState = 'IN_PROGRESS';

const resultPromise = executeRemove(amplify, input, cancellationToken);
const wrappedPromise = resultPromise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see if it makes sense to create a factory function for "AbortablePromise / AbortableTask", which consolidates the result/cancel function with promises.

But some tasks might not have pause/resume on them aka "NonPausableTransferTask"


await deleteObject(
{
...s3Config,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here you can add the abortSignal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-tests run the pr-label workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Amplify Storage remove folder

3 participants