Add multi-bucket support - #209
Merged
Merged
Conversation
The bucket was fixed at connect time: reaching another one meant clearing the session and typing the keys again. `switchBucket(bucketName, region?)` moves the session to another bucket on the credentials it already has. It builds a new BYOS3ApiProvider rather than calling setBucketName, because identity is what everything downstream watches to know its world has changed - the search service memoises on it, the download store keys off it, the upload executor compares against it. Renaming the bucket underneath them leaves all three serving the old bucket while the app shows the new one. The candidate is proved with the same one-object listing /connect uses before anything of the current session is disposed, so a bucket that is missing, in another region, or unreadable by these keys costs the user nothing. Only after it verifies does the existing initializeUploadManagers teardown run, which stays the one canonical path for rebuilding bucket-scoped services. Switching cancels every upload and delete in flight, so it refuses to do that unasked: it answers with the counts instead, and proceeds once the caller passes discardActiveWork. The prefix is dropped rather than carried across, since it names a folder in the bucket being left.
The s3-api has listed buckets since 3.0 and nothing in the app ever called it. `useBuckets` is the data layer the switcher will read from: it discovers, searches and pages, and does nothing else - selecting a bucket stays `useAuth().switchBucket`, which owns the S3 identity outright. Nothing is requested until the switcher asks. The dashboard mounts this on every page while hardly anyone opens it, and ListBuckets is billed, so the fetch is gated on the provider discovery was started for rather than a flag - a flag still reads true during the render that swapped the provider, and fired a request for a switcher nobody had opened. A request already in flight is joined rather than repeated, which also stops React Strict Mode buying every development open twice. Searching is layered because the server side of it is weaker than it looks. ListBuckets matches Prefix byte for byte, so it finds production-eu and misses my-production, and several S3-compatible providers ignore the parameter entirely. The server narrows, then a case-insensitive contains decides what is shown. A term with capitals is not sent at all: bucket names are lowercase, so the server would have thrown away the very rows the filter exists to find. Whether more pages exist is the server's word and never the number of rows the filter left visible, or a page whose matches are all further down would be unreachable. Provider identity is the cache identity, checked as the list is read rather than in an effect, so a switch cannot show the previous session's buckets even for one frame. A failure to list is not a broken session: listing needs s3:ListAllMyBuckets, which the connect guides never ask for, and some providers do not implement the call. That surfaces as `unavailable` for the switcher to offer a bucket name field instead, and touches neither the credentials nor the current bucket.
The bucket a session works in was only visible on the connect form that set it. A switcher now sits beside the menu button, naming the current bucket and opening a list of the others: search at the top, the current one marked, the region beside each bucket that reported one. It is strictly a view. `useBuckets` finds the candidates and `useAuth().switchBucket` performs the switch and owns everything behind it, so nothing here verifies, navigates, disposes managers or keeps a second opinion about which bucket is current. The list is asked for when the menu opens and never on mount: ListBuckets is billed and the dashboard mounts this on every page. Choosing a bucket that has uploads or deletes running is answered with a question rather than silently cancelling them, and the second call only goes out once someone has said yes. A bucket the provider will not verify leaves the session where it was and says why. Where a connection cannot list buckets at all - the permission for it is not one the connect guides ask for - the panel offers a name to type instead of implying the session is broken. Two things a menu does not expect an input to be inside it for had to be arranged deliberately: the field is given the opening focus, because taking it back afterwards loses a race with the menu and sends every keystroke to its typeahead, and the first arrow key hands the list a row to start its own navigation from, because a menu only acts on arrows aimed at itself. Switching also now clears the operations card. Aborting the work in flight left every record of it behind, so the previous bucket's uploads and deletes came across with it, naming files that are not in the bucket on screen.
Bucket regions were missing from the one listing that matters most. S3 fills in a bucket's region only when the ListBuckets request carries at least one valid parameter, so searching returned regions and simply opening the switcher did not - and switching to a bucket in another region built a client for the region being left, which then failed to verify for no reason the user could see. getBuckets now sends MaxBuckets on every request. It is 10,000, the page size S3 already applies by default, so it changes what comes back rather than how much. Providers that ignore parameters they do not implement are unaffected, and a region that goes unreported stays unreported rather than being guessed at: the frontend passes undefined through to switchBucket, which keeps the current region, and a bucket that turns out to be elsewhere fails verification with the session intact. Documents the permission split the feature introduced. Listing buckets needs s3:ListAllMyBuckets, granted on the account rather than a bucket, and none of the four permissions the connect guides ask for imply it. Switching does not need it: the check run against the bucket being moved to is the same single listing the connect form makes. So the guide now says which of the three things - browsing one bucket, seeing a list to pick from, switching by typing a name - needs what, and the developer notes record the search, pagination, region and provider-compatibility contract of getBuckets.
The switcher's footer offered a field for typing the name of a bucket that already existed, which only helped people who already had one. It now opens a form that creates one, and each row in the list can be deleted. Names are checked against S3's own rules before a request is spent on them, and each rule says which one was broken - an InvalidBucketName error names none of them. A dot is legal and stays legal, but is warned about: the wildcard certificate does not match a dotted bucket, so HTTPS to one fails in some tools. Region is a real choice only on AWS. S3 refuses a CreateBucket whose LocationConstraint does not match the regional endpoint the request arrived at, and the SDK has no per-request region override, so a region argument passed down to createBucket would produce the very error it appears to solve. A provider is built for the chosen region instead, the same way switchBucket already does it, and thrown away afterwards - creating a bucket elsewhere does not move the session there. Every other provider stores a resolved endpoint, and that URL sets the location, so the region is named rather than offered. The absence of a stored endpoint is the signal, since the provider slug is not persisted. Deleting is offered on every bucket but the one in use, which would leave the session addressing something that is not there. It asks first, and S3's refusal to remove a non-empty bucket is reported rather than worked around: nothing here empties a bucket on someone's behalf. Both update the loaded list in place rather than listing buckets again, which is billed and would answer with what the call that just succeeded already knows. The delete control is a sibling menu item rather than a button nested in the row, because a menu's roving focus only visits its own items - nested, it would have been clickable and unreachable by keyboard. Also defines --popover, which nothing did. DropdownMenuContent paints itself with bg-popover, so every menu in the app rendered as text over whatever was behind it, and only the profile menu escaped by passing a background of its own.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Copilot stopped work on behalf of
Rakesh-46-VR due to an error
September 3, 2026 18:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before this, you were stuck in one bucket. The bucket was set when you connected, and the only way to reach another one was to log out and type your keys again.
What you can do now
Switch buckets. The top bar shows the bucket you are in. Click it to see your other buckets, search them, and pick one.
Make a new bucket. The menu has a "New bucket" button. Type a name, pick a region, done.
Delete a bucket. Every bucket in the list has a delete button, except the one you are currently in.
Things we were careful about