Skip to content

Commit 5ada8f3

Browse files
committed
fix: pass credential provider for object operations
make part 5 mb each.
1 parent 4869000 commit 5ada8f3

7 files changed

Lines changed: 95 additions & 56 deletions

File tree

package-lock.json

Lines changed: 24 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"@aws-sdk/credential-providers": "^3.1000.0",
8282
"@smithy/shared-ini-file-loader": "^4.4.5",
8383
"@tigrisdata/iam": "^1.3.0",
84-
"@tigrisdata/storage": "^2.15.1",
84+
"@tigrisdata/storage": "^2.15.2",
8585
"axios": "^1.13.6",
8686
"commander": "^14.0.3",
8787
"enquirer": "^2.4.1",

src/auth/s3-client.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,17 @@ export type TigrisStorageConfig = {
5454
organizationId?: string;
5555
iamEndpoint?: string;
5656
authDomain?: string;
57+
credentialProvider?: () => Promise<{
58+
accessKeyId: string;
59+
secretAccessKey: string;
60+
sessionToken?: string;
61+
expiration?: Date;
62+
}>;
5763
};
5864

59-
export async function getStorageConfig(): Promise<TigrisStorageConfig> {
65+
export async function getStorageConfig(options?: {
66+
withCredentialProvider?: boolean;
67+
}): Promise<TigrisStorageConfig> {
6068
// 1. AWS profile (only if AWS_PROFILE is set)
6169
if (hasAwsProfile()) {
6270
const profile = process.env.AWS_PROFILE || 'default';
@@ -78,7 +86,6 @@ export async function getStorageConfig(): Promise<TigrisStorageConfig> {
7886

7987
if (loginMethod === 'oauth') {
8088
const authClient = getAuthClient();
81-
const accessToken = await authClient.getAccessToken();
8289
const selectedOrg = getSelectedOrganization();
8390

8491
if (!selectedOrg) {
@@ -88,9 +95,20 @@ export async function getStorageConfig(): Promise<TigrisStorageConfig> {
8895
}
8996

9097
return {
91-
sessionToken: accessToken,
98+
sessionToken: await authClient.getAccessToken(),
9299
accessKeyId: '',
93100
secretAccessKey: '',
101+
// Only include credentialProvider for long-running operations (uploads)
102+
// that need token refresh. Short-lived operations (ls, rm, head) use
103+
// the static sessionToken above and benefit from S3Client caching.
104+
...(options?.withCredentialProvider && {
105+
credentialProvider: async () => ({
106+
accessKeyId: '',
107+
secretAccessKey: '',
108+
sessionToken: await authClient.getAccessToken(),
109+
expiration: new Date(Date.now() + 10 * 60 * 1000),
110+
}),
111+
}),
94112
endpoint: tigrisConfig.endpoint,
95113
organizationId: selectedOrg,
96114
iamEndpoint: tigrisConfig.iamEndpoint,
@@ -132,7 +150,7 @@ export async function getStorageConfig(): Promise<TigrisStorageConfig> {
132150

133151
// No valid auth method found — try auto-login in interactive terminals
134152
if (await triggerAutoLogin()) {
135-
return getStorageConfig();
153+
return getStorageConfig(options);
136154
}
137155
throw new Error(
138156
'Not authenticated. Please run "tigris login" or "tigris configure" first.'
@@ -164,7 +182,6 @@ export async function getS3Client(): Promise<S3Client> {
164182

165183
if (loginMethod === 'oauth') {
166184
const authClient = getAuthClient();
167-
const accessToken = await authClient.getAccessToken();
168185
const selectedOrg = getSelectedOrganization();
169186

170187
if (!selectedOrg) {
@@ -173,14 +190,17 @@ export async function getS3Client(): Promise<S3Client> {
173190
);
174191
}
175192

193+
const credentialProvider = async () => ({
194+
accessKeyId: '',
195+
secretAccessKey: '',
196+
sessionToken: await authClient.getAccessToken(),
197+
expiration: new Date(Date.now() + 10 * 60 * 1000),
198+
});
199+
176200
const client = new S3Client({
177201
region: 'auto',
178202
endpoint: tigrisConfig.endpoint,
179-
credentials: {
180-
sessionToken: accessToken,
181-
accessKeyId: '', // Required by SDK but not used with token auth
182-
secretAccessKey: '', // Required by SDK but not used with token auth
183-
},
203+
credentials: credentialProvider,
184204
});
185205

186206
// Add middleware to inject custom headers

src/lib/cp.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getStorageConfig } from '../auth/s3-client.js';
2323
import { formatSize } from '../utils/format.js';
2424
import { get, put, list, head } from '@tigrisdata/storage';
2525
import { executeWithConcurrency } from '../utils/concurrency.js';
26+
import { calculateUploadParams } from '../utils/upload.js';
2627
import type { ParsedPath } from '../types.js';
2728

2829
type CopyDirection = 'local-to-remote' | 'remote-to-local' | 'remote-to-remote';
@@ -109,12 +110,8 @@ async function uploadFile(
109110
const fileStream = createReadStream(localPath);
110111
const body = Readable.toWeb(fileStream) as ReadableStream;
111112

112-
const useMultipart = fileSize !== undefined && fileSize > 16 * 1024 * 1024;
113-
114113
const { error: putError } = await put(key, body, {
115-
multipart: useMultipart,
116-
partSize: useMultipart ? 16 * 1024 * 1024 : undefined,
117-
queueSize: useMultipart ? 8 : undefined,
114+
...calculateUploadParams(fileSize),
118115
onUploadProgress: showProgress
119116
? ({ loaded }) => {
120117
if (fileSize !== undefined && fileSize > 0) {
@@ -246,12 +243,8 @@ async function copyObject(
246243
return { error: getError.message };
247244
}
248245

249-
const useMultipart = fileSize !== undefined && fileSize > 16 * 1024 * 1024;
250-
251246
const { error: putError } = await put(destKey, data, {
252-
multipart: useMultipart,
253-
partSize: useMultipart ? 16 * 1024 * 1024 : undefined,
254-
queueSize: useMultipart ? 8 : undefined,
247+
...calculateUploadParams(fileSize),
255248
onUploadProgress: showProgress
256249
? ({ loaded }) => {
257250
if (fileSize !== undefined && fileSize > 0) {
@@ -749,7 +742,7 @@ export default async function cp(options: Record<string, unknown>) {
749742

750743
const recursive = !!getOption<boolean>(options, ['recursive', 'r']);
751744
const direction = detectDirection(src, dest);
752-
const config = await getStorageConfig();
745+
const config = await getStorageConfig({ withCredentialProvider: true });
753746

754747
switch (direction) {
755748
case 'local-to-remote': {

src/lib/mv.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getOption } from '../utils/options.js';
1111
import { getStorageConfig } from '../auth/s3-client.js';
1212
import { formatSize } from '../utils/format.js';
1313
import { get, put, remove, list, head } from '@tigrisdata/storage';
14+
import { calculateUploadParams } from '../utils/upload.js';
1415

1516
async function confirm(message: string): Promise<boolean> {
1617
const rl = readline.createInterface({
@@ -66,7 +67,7 @@ export default async function mv(options: Record<string, unknown>) {
6667
process.exit(1);
6768
}
6869

69-
const config = await getStorageConfig();
70+
const config = await getStorageConfig({ withCredentialProvider: true });
7071

7172
// Check if source is a single object or a prefix (folder/wildcard)
7273
const isWildcard = srcPath.path.includes('*');
@@ -338,17 +339,14 @@ async function moveObject(
338339
return {};
339340
}
340341

341-
// Get source object size for progress
342-
let fileSize: number | undefined;
343-
if (showProgress) {
344-
const { data: headData } = await head(srcKey, {
345-
config: {
346-
...config,
347-
bucket: srcBucket,
348-
},
349-
});
350-
fileSize = headData?.size;
351-
}
342+
// Get source object size for upload params and progress
343+
const { data: headData } = await head(srcKey, {
344+
config: {
345+
...config,
346+
bucket: srcBucket,
347+
},
348+
});
349+
const fileSize = headData?.size;
352350

353351
// Get source object
354352
const { data, error: getError } = await get(srcKey, 'stream', {
@@ -362,12 +360,9 @@ async function moveObject(
362360
return { error: getError.message };
363361
}
364362

365-
// Use multipart for files larger than 100MB
366-
const useMultipart = fileSize !== undefined && fileSize > 100 * 1024 * 1024;
367-
368363
// Put to destination
369364
const { error: putError } = await put(destKey, data, {
370-
multipart: useMultipart,
365+
...calculateUploadParams(fileSize),
371366
onUploadProgress: showProgress
372367
? ({ loaded }) => {
373368
if (fileSize !== undefined && fileSize > 0) {

src/lib/objects/put.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
printFailure,
1111
msg,
1212
} from '../../utils/messages.js';
13+
import { calculateUploadParams } from '../../utils/upload.js';
1314

1415
const context = msg('objects', 'put');
1516

@@ -65,18 +66,17 @@ export default async function putObject(options: Record<string, unknown>) {
6566
body = Readable.toWeb(process.stdin) as ReadableStream;
6667
}
6768

68-
const config = await getStorageConfig();
69+
const config = await getStorageConfig({ withCredentialProvider: true });
6970

70-
// Use multipart upload for files larger than 16MB (or always for stdin)
71-
const useMultipart =
72-
!file || (fileSize !== undefined && fileSize > 16 * 1024 * 1024);
71+
// For stdin (no file), always use multipart since we don't know the size
72+
const uploadParams = file
73+
? calculateUploadParams(fileSize)
74+
: { multipart: true, partSize: 5 * 1024 * 1024, queueSize: 8 };
7375

7476
const { data, error } = await put(key, body, {
7577
access: access === 'public' ? 'public' : 'private',
7678
contentType,
77-
multipart: useMultipart,
78-
partSize: useMultipart ? 16 * 1024 * 1024 : undefined,
79-
queueSize: useMultipart ? 8 : undefined,
79+
...uploadParams,
8080
onUploadProgress: ({ loaded, percentage }) => {
8181
if (fileSize !== undefined && fileSize > 0) {
8282
process.stdout.write(

src/utils/upload.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const DEFAULT_PART_SIZE = 5 * 1024 * 1024; // 5 MB
2+
const MAX_PARTS = 10_000; // S3 hard limit
3+
const DEFAULT_QUEUE_SIZE = 10; // matches AWS CLI max_concurrent_requests
4+
5+
export function calculateUploadParams(fileSize?: number) {
6+
if (!fileSize || fileSize <= DEFAULT_PART_SIZE) {
7+
return { multipart: false } as const;
8+
}
9+
10+
let partSize = DEFAULT_PART_SIZE;
11+
12+
// Increase part size if needed to stay under S3's 10K part limit
13+
if (fileSize / partSize > MAX_PARTS) {
14+
partSize = Math.ceil(fileSize / MAX_PARTS);
15+
}
16+
17+
return { multipart: true, partSize, queueSize: DEFAULT_QUEUE_SIZE } as const;
18+
}

0 commit comments

Comments
 (0)