mirror of
https://github.com/alexandernicholson/s3panoramic.git
synced 2026-03-31 09:07:11 +09:00
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user