Skip to content

Add S3 PSDrive provider and new Mount-S3PSDrive/Dismount-S3PSDrive cmdlets to AWS.Tools.S3 - #423

Open
andliao445 wants to merge 9 commits into
developmentfrom
psdrive-for-s3
Open

Add S3 PSDrive provider and new Mount-S3PSDrive/Dismount-S3PSDrive cmdlets to AWS.Tools.S3#423
andliao445 wants to merge 9 commits into
developmentfrom
psdrive-for-s3

Conversation

@andliao445

@andliao445 andliao445 commented Jul 15, 2026

Copy link
Copy Markdown

Description

Adds an S3 PowerShell drive provider (AWS.S3) to AWS.Tools.S3. After
Mount-S3PSDrive -Name S3, users navigate and operate on S3 with the standard
provider cmdlets — Set-Location, Get-ChildItem, Get-Item, Get-Content,
Set-Content, Remove-Item, and Dismount-S3PSDrive — across buckets, prefixes
(folders), and objects (files).

Highlights:

  • A NavigationCmdletProvider + IContentCmdletProvider that calls the AWS SDK
    directly (it does not invoke the S3 cmdlets).
  • Streamed, paginated listings backed by a short-TTL listing cache and an
    existence-probe cache that collapses the engine's repeated per-command
    path-resolution probes.
  • Content via TransferUtility (multipart upload/download); Ctrl+C cancels an
    in-flight transfer.
  • One drive spans all regions (each bucket's region is resolved and cached); an
    optional -Root scopes a drive to a bucket or bucket+prefix.
  • Credentials/region reuse AWS.Tools.Common's session defaults
    ($StoredAWSCredentials / $StoredAWSRegion), with explicit drive parameters
    taking precedence; credentials then fall back to the SDK default chain.
  • -StorageClass drive default plus per-upload override; -Encoding,
    -AsByteStream, and -NoNewline on content operations.

Also included:

  • A generator fix so the advanced-cmdlet scanner accepts a cmdlet verb supplied
    via a VerbsData.* member-access expression (needed for Mount-S3PSDrive;
    the scanner previously handled only string-literal verbs/nouns).
  • A new S3ItemInfo table format view for Get-ChildItem output.

Motivation and Context

Lets users browse and manipulate S3 as a mounted drive — matching the built-in
FileSystem provider experience — instead of composing individual
Get-S3Object / Write-S3Object calls.

Testing

Live Pester integration tests in tests/S3/S3.PSDrive.Tests.ps1, run against real
S3 (us-east-1, plus us-west-2 for the cross-region case): root/bucket/prefix
listing with pagination, Get-Item on all three item kinds, content round-trips
(text and byte, encodings, BOM, -Raw, -NoNewline), single and recursive
Remove-Item, storage-class default + per-upload override, -Root prefix
mounting, session-default credential/region fallback, cache-invalidation and
existence-probe correctness, name-collision handling, and unsupported-op
rejection (Copy-Item, Add-Content). The suite rides the shared repo harness
(test-runner profile, Smoke tag) like the sibling tests/S3/*.Tests.ps1.

Dry-run

  • Dry-run ID: 11cf01fd-e672-41ea-be65-430c2d373a78
  • Status:
    • Pending
    • Completed successfully
    • Failed
  • Failed bypass reason: N/A

Breaking Changes Assessment

No breaking changes. This is purely additive — two new cmdlets and a new provider
in AWS.Tools.S3; no existing cmdlet, parameter, or behavior is modified. The
generator scanner change is backward-compatible: it adds handling for a new
attribute-argument shape (member access), leaving existing string-literal cmdlets
unaffected.

Screenshots (if appropriate)

N/A

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code builds in Gamma and passes backward compatibility validation (required)
  • My code follows the code style of this project
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have read the README document
  • I have added tests to cover my changes
  • All new and existing tests passed

New/existing dependencies impact assessment, if applicable

No new dependencies. The provider uses the AWSSDK.S3 / AWSSDK.Core assemblies
AWS.Tools.S3 already references.

License

  • I confirm that this pull request can be released under the Apache 2 license

@andliao445
andliao445 requested review from a team as code owners July 15, 2026 23:43
@andliao445
andliao445 requested review from afroz429 and sankettangade and removed request for a team July 15, 2026 23:43
@andliao445
andliao445 force-pushed the psdrive-for-s3 branch 3 times, most recently from 77ce443 to 4a1344b Compare July 20, 2026 20:46
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3Provider.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3Provider.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.cs
Comment thread .gitignore Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3ListingCache.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.cs
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3TransferContentWriter.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3TransferContentWriter.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3Provider.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3Provider.cs Outdated
@sankettangade

sankettangade commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

In C#, a partial class lets you spread one class across multiple files. The type is identical, and no behavior changes at all. You're just splitting one large file into smaller, focused files.

Suggestions: Map your existing regions onto files like this. (You can explore other ways or can decide on the split structure)

Cmdlets/S3/Drive/
├── Provider/                       ← the partial S3Provider, one concern per file
│   ├── S3Provider.cs               // class decl, fields, Drive/Client props
│   ├── S3Provider.Drive.cs         // NewDrive, ValidateRoot, RemoveDrive
│   ├── S3Provider.Navigation.cs    // MakePath, IsItemContainer, etc.
│   ├── S3Provider.Listing.cs       // GetChildItems/Names, StreamChildren/AllUnder
│   ├── S3Provider.Item.cs          // GetItem, ItemExists
│   ├── S3Provider.RemoveItem.cs    // RemoveItem, RemovePrefixRecursive, DeleteBatch
│   ├── S3Provider.Content.cs       // GetContentReader/Writer + dynamic params
│   ├── S3Provider.Credentials.cs   // ResolveRegion/Credentials, session defaults
│   ├── S3Provider.Path.cs          // ParsePath, ApplyDriveRoot, MakeChildPath
│   └── S3Provider.Cancellation.cs  // RunSync, StopProcessing, CTS tracking
│
├── S3DriveInfo.cs                  // supporting types stay in Drive/
├── S3DriveParameters.cs
├── S3ItemInfo.cs
├── S3ListingCache.cs
├── S3ContentReader.cs
├── S3ContentDynamicParameters.cs
├── S3TransferContentWriter.cs
├── PushPullStream.cs
└── S3Cancellation.cs

This improves code readability and also helps a reviewer who wants to understand how does delete work? can open one focused file instead of scrolling through 2k+ lines to find it. Each file becomes independently readable.

@sankettangade

Copy link
Copy Markdown
Contributor

In C#, a partial class lets you spread one class across multiple files. The type is identical, and no behavior changes at all. You're just splitting one large file into smaller, focused files.

Suggestions: Map your existing regions onto files like this. (You can explore other ways or can decide on the split structure)

Cmdlets/S3/Drive/
├── Provider/                       ← the partial S3Provider, one concern per file
│   ├── S3Provider.cs               // class decl, fields, Drive/Client props
│   ├── S3Provider.Drive.cs         // NewDrive, ValidateRoot, RemoveDrive
│   ├── S3Provider.Navigation.cs    // MakePath, IsItemContainer, etc.
│   ├── S3Provider.Listing.cs       // GetChildItems/Names, StreamChildren/AllUnder
│   ├── S3Provider.Item.cs          // GetItem, ItemExists
│   ├── S3Provider.RemoveItem.cs    // RemoveItem, RemovePrefixRecursive, DeleteBatch
│   ├── S3Provider.Content.cs       // GetContentReader/Writer + dynamic params
│   ├── S3Provider.Credentials.cs   // ResolveRegion/Credentials, session defaults
│   ├── S3Provider.Path.cs          // ParsePath, ApplyDriveRoot, MakeChildPath
│   └── S3Provider.Cancellation.cs  // RunSync, StopProcessing, CTS tracking
│
├── S3DriveInfo.cs                  // supporting types stay in Drive/
├── S3DriveParameters.cs
├── S3ItemInfo.cs
├── S3ListingCache.cs
├── S3ContentReader.cs
├── S3ContentDynamicParameters.cs
├── S3TransferContentWriter.cs
├── PushPullStream.cs
└── S3Cancellation.cs

This improves code readability and also helps a reviewer who wants to understand how does delete work? can open one focused file instead of scrolling through 2k+ lines to find it. Each file becomes independently readable.

The current changes look good, but can you update the structure to somewhat like this? A subfolder for all the Provider files? This looks more clean.

Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.Credentials.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.Credentials.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.Item.cs
Comment thread tests/S3/S3.PSDrive.Tests.ps1
@andliao445
andliao445 force-pushed the psdrive-for-s3 branch 2 times, most recently from 3735a02 to 54e61d0 Compare July 28, 2026 00:19
{
// Empty objects can reject the SDK's initial ranged GET. Fall back to the
// ordinary stream path; there is nothing to parallelize for an empty object.
return tu.OpenStreamAsync(new TransferUtilityOpenStreamRequest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this seems like a bug in the transfer utility. i dont think we should do this fallback behavior here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I agree this probably belongs in TransferUtility, but without this fallback Get-Content breaks for zero-byte objects because the SDK’s ranged discovery can return InvalidRange before it falls back to a normal stream.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let me see if i can fix this in transfer utility

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/S3TransferContentWriter.cs Outdated
Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.Content.cs Outdated
// Default part size for Set-Content's non-seekable stream uploads. TU cannot choose from
// the final object length, so this keeps streams under S3's 10,000-part limit until ~156 GiB.
// Set-Content -PartSize overrides it.
private const long DefaultMultipartUploadPartSize = 16L * 1024 * 1024;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is this here? doesnt seem like its used?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It's used at S3Provider.cs:200 but the github diff just cut it off I think. Also at S3Provider.Content.cs:298

// Set-Content -PartSize overrides it.
private const long DefaultMultipartUploadPartSize = 16L * 1024 * 1024;

private Amazon.S3.Transfer.TransferUtility TransferUtilityForBucket(S3DriveInfo drive, string bucket) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

where is TransferUtilityForBucket used though

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

in GetContentReader at S3Provider.Content.cs:97 and GetContentWriter at S3Provider.Content.cs:322

Comment thread modules/AWSPowerShell/Cmdlets/S3/Drive/Provider/S3Provider.cs Outdated
@afroz429

afroz429 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

During testing, found a potential data loss issue. Remove-Item S3T:\bucket\prefix -Filter *.log -Recurse deletes all objects, including .txt.

Consider rejecting -Filter on Remove-Item with a clear error or apply it in RemovePrefixRecursive's per-key loop (same MatchesFilter used on listing). Add a test asserting Remove-Item <prefix> -Filter *.log -Recurse leaves the non-matching objects.

@andliao445
andliao445 force-pushed the psdrive-for-s3 branch 5 times, most recently from 16a649e to 0f2ea93 Compare August 5, 2026 22:58
while (!string.IsNullOrEmpty(token));

// Also delete the object at the exact key: when a name is both folder ("key/...") and
// object ("key"), the sweep above only covers "key/", leaving the shadowed object behind.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When recursively removing the folder data/, this also deletes an object literally named data if one exists as separate, unrelated content and since ShouldProcess only prompted for the prefix, a -Confirm'd user wouldn't see that extra object being deleted.

Fine as a deliberate delete, but wanted to flag it since it can remove data the user didn't ask for. Your call on whether to gate it or throw a warning about the object also being deleted with the same key name.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added warning

Introduces the AWS.S3 PowerShell provider so buckets, prefixes, and objects can be
browsed with the standard navigation commands, plus the Mount/Dismount-S3PSDrive
cmdlets. Extends the advanced cmdlet scanner to recognize member-access verb/noun
pairs, and adds the integration test suite for the provider.
…w pass

Splits the cmdlets, parameters, and provider into one type per file and moves the
provider partials under a Provider folder. Adds a configurable upload part size and
support for pipeline-bound Set-Content writer parameters. Folds in the first round of
review feedback (capability checks, region fallback, pagination, drive safety) and
makes the tests run headless in CI, tagging the expensive live tests so they stay out
of the daily pipeline.
…ect handling

Resolves provider-qualified paths across multiple mounted drives and switches reads
to the TransferUtility multipart stream. Surfaces the guided SSO login error and
re-resolves rotated profile credentials. Makes folders win over colliding objects,
returns a single item for Get-Item on the drive root, adds -Filter as a leaf-name
wildcard, gives a consistent error for unsupported operations, validates -StorageClass
at mount, and uses the OS-native separator in path errors.
…tion

Adds the download part size parameter with validation and the Get-Help content for
the cmdlets. Makes dismount errors deterministic and fixes profile rotation
resolution.
… cleanup

Fixes the TransferUtility part size handling and removes the zero-byte object fallback
now that the SDK handles it. Updates the part-size test for the 5 MiB default and
shortens the code comments to match the team's style.
Set-Content used to feed TransferUtility a non-seekable bridge stream for
every write, which forced TU's multipart path (initiate + part + complete)
even for a few KB. The writer now buffers content and decides at Close:
under 5 MiB it hands TU a seekable MemoryStream so TU does a single
PutObject, and at or over 5 MiB it escalates to the streaming multipart
bridge, which is the old behavior unchanged.

5 MiB is S3's smallest multipart part and is under TU's 16 MiB
MinSizeBeforePartUpload, so a seekable stream below it is guaranteed to
become one PutObject. The threshold is a fixed internal bound, not the
user's -PartSize, so the buffering path stays memory-bounded. The simple
upload sets AutoResetStreamPosition so the SDK rereads the buffer from the
start on the first send and any retry.

Adds a "Small-write single PutObject" test context that checks the upload
path by the stored object's ETag (dashless = one PutObject, dashed =
multipart) across 0 B to 10 MiB, plus -StorageClass forwarding and
escalation at exactly 5 MiB. Also updates the now-stale GetContentWriter
comment that said every upload goes multipart.

Small-write median drops from ~693 ms to ~314 ms, closing the gap to
Write-S3Object from 2.6x to 1.2x.
Remove-Item on the S3 drive advertises -Filter (ProviderCapabilities.Filter)
and the listing path honors it, but the delete path ignored it: a recursive
delete listed every key under the prefix and deleted them all. So
Remove-Item <prefix> -Filter *.log -Recurse deleted every object, including
the ones the filter was meant to spare. That's a data-loss bug.

Apply the existing MatchesFilter(LeafName(...)) in the recursive delete loop,
the shadowed-exact-key delete, and the single-object path, matching how
Get-ChildItem -Filter -Recurse scopes by leaf name. With no filter set,
MatchesFilter returns true, so unfiltered deletes still remove everything.

Adds two tests: a filtered recursive delete leaves non-matching and nested
objects intact, and an unfiltered recursive delete still removes everything.
A recursive Remove-Item on a prefix already deletes an object sharing the
prefix's name (folder-wins hides it from reads and single-level listings, so
the exact-key sweep would otherwise orphan it). It now HEAD-checks that key and,
when a real same-named object exists, warns that both were removed, so a
-Confirm user who approved removing the folder sees the extra deletion.
…und"

ItemExists, IsItemContainer, and HasChildItems caught NotFound and
AccessDenied but let invalid/expired-credential errors propagate. The
provider engine turns any throw from those into "Cannot find path ...
does not exist", so an expired token surfaced as a bogus not-found.

Catch IsInvalidCredentials in all three (like AccessDenied) so path
resolution succeeds and the actual operation surfaces the genuine error.
Mount still fails fast: ValidateRoot lets these propagate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants