fix: refresh credentials
Some checks failed
Docker / build (push) Has been cancelled

This commit is contained in:
Alexander Nicholson 4584443+DragonStuff@users.noreply.github.com
2024-11-20 21:07:15 +09:00
parent b55a72fd6e
commit 3e55fd6cc1

View File

@@ -18,6 +18,8 @@ interface StorageServiceConfig {
export class StorageService { export class StorageService {
private client: S3; private client: S3;
private credentials!: Credentials; private credentials!: Credentials;
private lastCredentialRefresh: number = 0;
private readonly CREDENTIAL_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
constructor(private config: StorageServiceConfig) { constructor(private config: StorageServiceConfig) {
this.initializeClient(); this.initializeClient();
@@ -114,68 +116,105 @@ export class StorageService {
throw new Error(`No valid AWS credentials found:\n${errors.join('\n')}`); throw new Error(`No valid AWS credentials found:\n${errors.join('\n')}`);
} }
private async refreshCredentialsIfNeeded() {
const now = Date.now();
if (now - this.lastCredentialRefresh >= this.CREDENTIAL_REFRESH_INTERVAL) {
this.credentials = await this.resolveCredentials();
this.lastCredentialRefresh = now;
// Reinitialize client with new credentials
const factory = new ApiFactory({
region: this.config.region,
credentials: this.credentials,
});
this.client = new S3(factory);
}
}
private async retryWithRefresh<T>(operation: () => Promise<T>): Promise<T> {
try {
await this.refreshCredentialsIfNeeded();
return await operation();
} catch (error) {
if (error instanceof Error &&
(error.message.includes("ExpiredToken") ||
error.message.includes("InvalidToken"))) {
// Force refresh credentials and retry once
this.lastCredentialRefresh = 0;
await this.refreshCredentialsIfNeeded();
return await operation();
}
throw error;
}
}
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> { async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
await this.ensureInitialized(); return await this.retryWithRefresh(async () => {
return await getSignedUrl({ await this.ensureInitialized();
accessKeyId: this.credentials.awsAccessKeyId, return await getSignedUrl({
secretAccessKey: this.credentials.awsSecretKey, accessKeyId: this.credentials.awsAccessKeyId,
sessionToken: this.credentials.sessionToken, secretAccessKey: this.credentials.awsSecretKey,
bucket: this.config.bucket, sessionToken: this.credentials.sessionToken,
key, bucket: this.config.bucket,
region: this.config.region, key,
expiresIn, region: this.config.region,
expiresIn,
});
}); });
} }
async listObjects(options: ListObjectsOptions): Promise<ListObjectsResult> { async listObjects(options: ListObjectsOptions): Promise<ListObjectsResult> {
await this.ensureInitialized(); return await this.retryWithRefresh(async () => {
try { await this.ensureInitialized();
const params: ListObjectsV2Request = { try {
Bucket: this.config.bucket, const params: ListObjectsV2Request = {
Prefix: options.prefix, Bucket: this.config.bucket,
Delimiter: options.delimiter, Prefix: options.prefix,
MaxKeys: options.maxKeys, Delimiter: options.delimiter,
ContinuationToken: options.continuationToken, MaxKeys: options.maxKeys,
}; ContinuationToken: options.continuationToken,
};
const response = await this.client.listObjectsV2(params); const response = await this.client.listObjectsV2(params);
const objects: StorageObject[] = response.Contents?.map(obj => ({ const objects: StorageObject[] = response.Contents?.map(obj => ({
key: obj.Key || "", key: obj.Key || "",
size: obj.Size || 0, size: obj.Size || 0,
lastModified: obj.LastModified || new Date(), lastModified: obj.LastModified || new Date(),
etag: (obj.ETag || "").replace(/^"|"$/g, ""), etag: (obj.ETag || "").replace(/^"|"$/g, ""),
contentType: undefined, // Content type requires separate HEAD request contentType: undefined,
})) || []; })) || [];
// Get content types in parallel for better performance await Promise.all(objects.map(async (obj) => {
await Promise.all(objects.map(async (obj) => { obj.contentType = await this.getContentType(obj.key);
obj.contentType = await this.getContentType(obj.key); }));
}));
return { return {
objects, objects,
prefixes: response.CommonPrefixes?.map(p => p.Prefix || "") || [], prefixes: response.CommonPrefixes?.map(p => p.Prefix || "") || [],
truncated: response.IsTruncated || false, truncated: response.IsTruncated || false,
nextContinuationToken: response.NextContinuationToken, nextContinuationToken: response.NextContinuationToken,
}; };
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`Failed to list objects: ${error instanceof Error ? error.message : String(error)}` `Failed to list objects: ${error instanceof Error ? error.message : String(error)}`
); );
} }
});
} }
private async getContentType(key: string): Promise<string | undefined> { private async getContentType(key: string): Promise<string | undefined> {
try { return await this.retryWithRefresh(async () => {
const response = await this.client.headObject({ try {
Bucket: this.config.bucket, const response = await this.client.headObject({
Key: key, Bucket: this.config.bucket,
}); Key: key,
return response.ContentType; });
} catch { return response.ContentType;
return undefined; } catch {
} return undefined;
}
});
} }
} }