Skip to content

Commit cad05dc

Browse files
committed
refactor: extract AssetBuckets class from asset-proxy.js
1 parent 1126949 commit cad05dc

File tree

4 files changed

+348
-335
lines changed

4 files changed

+348
-335
lines changed

src/lib/asset-buckets.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
ListBucketsCommand,
3+
HeadBucketCommand
4+
} from '@aws-sdk/client-s3'
5+
import { s3 } from './aws-clients.js'
6+
import logger from './logger.js'
7+
8+
const s3Client = s3()
9+
10+
export const BucketOptionEnum = Object.freeze({
11+
NONE: 'NONE',
12+
ALL: 'ALL',
13+
ALL_BUCKETS_IN_ACCOUNT: 'ALL_BUCKETS_IN_ACCOUNT',
14+
LIST: 'LIST'
15+
})
16+
17+
export class AssetBuckets {
18+
/**
19+
* @param {string} bucketOption - Bucket option (NONE, ALL, ALL_BUCKETS_IN_ACCOUNT, LIST)
20+
* @param {string[]|null} bucketNames - Array of bucket names (required for LIST option)
21+
*/
22+
constructor(bucketOption, bucketNames) {
23+
this.bucketOption = bucketOption
24+
this.bucketNames = bucketNames
25+
this.bucketCache = {}
26+
}
27+
28+
/**
29+
* @param {string} bucketOption - Bucket option (NONE, ALL, ALL_BUCKETS_IN_ACCOUNT, LIST)
30+
* @param {string[]|null} bucketNames - Array of bucket names (required for LIST option)
31+
* @returns {Promise<AssetBuckets>} Initialized AssetBuckets instance
32+
*/
33+
static async create(bucketOption, bucketNames) {
34+
const instance = new AssetBuckets(bucketOption, bucketNames)
35+
await instance._initBuckets()
36+
return instance
37+
}
38+
39+
/**
40+
* @returns {Promise<void>}
41+
*/
42+
async _initBuckets() {
43+
switch (this.bucketOption) {
44+
case BucketOptionEnum.LIST: {
45+
if (this.bucketNames && this.bucketNames.length > 0) {
46+
await Promise.all(
47+
this.bucketNames.map(async (name) => { await this.getBucket(name) })
48+
)
49+
50+
const invalidBuckets = Object.keys(this.bucketCache)
51+
.filter((bucketName) => this.bucketCache[bucketName].region === null)
52+
if (invalidBuckets.length > 0) {
53+
throw new Error(
54+
`Could not access or determine region for the following buckets: ${
55+
invalidBuckets.join(', ')}`
56+
)
57+
}
58+
59+
const count = Object.keys(this.bucketCache).length
60+
logger.info(
61+
`Parsed ${count} buckets from ASSET_PROXY_BUCKET_LIST for asset proxy`
62+
)
63+
} else {
64+
throw new Error(
65+
'ASSET_PROXY_BUCKET_LIST must not be empty when ASSET_PROXY_BUCKET_OPTION is LIST'
66+
)
67+
}
68+
break
69+
}
70+
71+
case BucketOptionEnum.ALL_BUCKETS_IN_ACCOUNT: {
72+
const command = new ListBucketsCommand({})
73+
const response = await s3Client.send(command)
74+
const buckets = response.Buckets || []
75+
76+
await Promise.all(
77+
buckets
78+
.map((bucket) => bucket.Name)
79+
.filter((name) => typeof name === 'string')
80+
.map(async (name) => { await this.getBucket(name) })
81+
)
82+
83+
const count = Object.keys(this.bucketCache).length
84+
logger.info(
85+
`Fetched ${count} buckets from AWS account for asset proxy`
86+
)
87+
break
88+
}
89+
90+
default:
91+
break
92+
}
93+
}
94+
95+
/**
96+
* @param {string} bucketName - S3 bucket name
97+
* @returns {Promise<Object>} Bucket info {name, region}
98+
*/
99+
async getBucket(bucketName) {
100+
if (!(bucketName in this.bucketCache)) {
101+
const command = new HeadBucketCommand({ Bucket: bucketName })
102+
const response = await s3Client.send(command)
103+
const statusCode = response.$metadata.httpStatusCode
104+
let name = null
105+
let region = null
106+
107+
switch (statusCode) {
108+
case 200:
109+
name = bucketName
110+
region = response.BucketRegion === 'EU'
111+
? 'eu-west-1'
112+
: response.BucketRegion || 'us-east-1'
113+
break
114+
case 403:
115+
logger.warn(`Access denied to bucket ${bucketName}`)
116+
break
117+
case 404:
118+
logger.warn(`Bucket ${bucketName} does not exist`)
119+
break
120+
case 400:
121+
logger.warn(`Bad request for bucket ${bucketName}`)
122+
break
123+
default:
124+
logger.warn(`Unexpected status code ${statusCode} for bucket ${bucketName}`)
125+
}
126+
127+
this.bucketCache[bucketName] = { name, region }
128+
}
129+
return this.bucketCache[bucketName]
130+
}
131+
132+
/**
133+
* @param {string} bucketName - S3 bucket name
134+
* @returns {boolean} True if bucket should be proxied, False otherwise
135+
*/
136+
shouldProxyBucket(bucketName) {
137+
if (this.bucketOption === BucketOptionEnum.ALL
138+
|| bucketName in this.bucketCache) {
139+
return true
140+
}
141+
return false
142+
}
143+
}

src/lib/asset-proxy.js

Lines changed: 4 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,29 @@
1-
/* eslint-disable max-classes-per-file */
21
import {
32
GetObjectCommand,
4-
ListBucketsCommand,
5-
HeadBucketCommand
63
} from '@aws-sdk/client-s3'
74
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
85
import { s3 } from './aws-clients.js'
96
import logger from './logger.js'
7+
import { AssetBuckets, BucketOptionEnum } from './asset-buckets.js'
108

119
const s3Client = s3()
1210

1311
const S3_URL_REGEX = /^s3:\/\/([^/]+)\/(.+)$/
1412

1513
export const ALTERNATE_ASSETS_EXTENSION = 'https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json'
1614

17-
export const BucketOption = Object.freeze({
18-
NONE: 'NONE',
19-
ALL: 'ALL',
20-
ALL_BUCKETS_IN_ACCOUNT: 'ALL_BUCKETS_IN_ACCOUNT',
21-
LIST: 'LIST'
22-
})
23-
2415
/**
2516
* @param {string} url - S3 URL to parse
2617
* @returns {Object} {bucket, key} or {bucket: null, key: null} if not a valid S3 URL
2718
*/
28-
const parseS3Url = (url) => {
19+
export const parseS3Url = (url) => {
2920
const match = S3_URL_REGEX.exec(url)
3021
if (!match) return { bucket: null, key: null }
3122

3223
const [, bucket, key] = match
3324
return { bucket, key }
3425
}
3526

36-
class AssetBuckets {
37-
/**
38-
* @param {string} bucketOption - Bucket option (NONE, ALL, ALL_BUCKETS_IN_ACCOUNT, LIST)
39-
* @param {string[]|null} bucketNames - Array of bucket names (required for LIST option)
40-
*/
41-
constructor(bucketOption, bucketNames) {
42-
this.bucketOption = bucketOption
43-
this.bucketNames = bucketNames
44-
this.buckets = {}
45-
}
46-
47-
/**
48-
* @param {string} bucketOption - Bucket option (NONE, ALL, ALL_BUCKETS_IN_ACCOUNT, LIST)
49-
* @param {string[]|null} bucketNames - Array of bucket names (required for LIST option)
50-
* @returns {Promise<AssetBuckets>} Initialized AssetBuckets instance
51-
*/
52-
static async create(bucketOption, bucketNames) {
53-
const instance = new AssetBuckets(bucketOption, bucketNames)
54-
await instance._initBuckets()
55-
return instance
56-
}
57-
58-
/**
59-
* @returns {Promise<void>}
60-
*/
61-
async _initBuckets() {
62-
switch (this.bucketOption) {
63-
case BucketOption.LIST: {
64-
if (this.bucketNames && this.bucketNames.length > 0) {
65-
await Promise.all(
66-
this.bucketNames.map(async (name) => { await this.getBucket(name) })
67-
)
68-
69-
const invalidBuckets = Object.keys(this.buckets)
70-
.filter((bucketName) => this.buckets[bucketName].region === null)
71-
if (invalidBuckets.length > 0) {
72-
throw new Error(
73-
`Could not access or determine region for the following buckets: ${
74-
invalidBuckets.join(', ')}`
75-
)
76-
}
77-
78-
const count = Object.keys(this.buckets).length
79-
logger.info(
80-
`Parsed ${count} buckets from ASSET_PROXY_BUCKET_LIST for asset proxy`
81-
)
82-
} else {
83-
throw new Error(
84-
'ASSET_PROXY_BUCKET_LIST must not be empty when ASSET_PROXY_BUCKET_OPTION is LIST'
85-
)
86-
}
87-
break
88-
}
89-
90-
case BucketOption.ALL_BUCKETS_IN_ACCOUNT: {
91-
const command = new ListBucketsCommand({})
92-
const response = await s3Client.send(command)
93-
const buckets = response.Buckets || []
94-
95-
await Promise.all(
96-
buckets
97-
.map((bucket) => bucket.Name)
98-
.filter((name) => typeof name === 'string')
99-
.map(async (name) => { await this.getBucket(name) })
100-
)
101-
102-
const count = Object.keys(this.buckets).length
103-
logger.info(
104-
`Fetched ${count} buckets from AWS account for asset proxy`
105-
)
106-
break
107-
}
108-
109-
default:
110-
break
111-
}
112-
}
113-
114-
/**
115-
* @param {string} bucketName - S3 bucket name
116-
* @returns {Promise<Object>} Bucket info {name, region}
117-
*/
118-
async getBucket(bucketName) {
119-
if (!(bucketName in this.buckets)) {
120-
const command = new HeadBucketCommand({ Bucket: bucketName })
121-
const response = await s3Client.send(command)
122-
const statusCode = response.$metadata.httpStatusCode
123-
let name = null
124-
let region = null
125-
126-
switch (statusCode) {
127-
case 200:
128-
name = bucketName
129-
region = response.BucketRegion === 'EU'
130-
? 'eu-west-1'
131-
: response.BucketRegion || 'us-east-1'
132-
break
133-
case 403:
134-
logger.warn(`Access denied to bucket ${bucketName}`)
135-
break
136-
case 404:
137-
logger.warn(`Bucket ${bucketName} does not exist`)
138-
break
139-
case 400:
140-
logger.warn(`Bad request for bucket ${bucketName}`)
141-
break
142-
default:
143-
logger.warn(`Unexpected status code ${statusCode} for bucket ${bucketName}`)
144-
}
145-
146-
this.buckets[bucketName] = { name, region }
147-
}
148-
return this.buckets[bucketName]
149-
}
150-
151-
/**
152-
* @param {string} bucketName - S3 bucket name
153-
* @returns {boolean} True if bucket should be proxied, False otherwise
154-
*/
155-
shouldProxyBucket(bucketName) {
156-
if (this.bucketOption === BucketOption.ALL
157-
|| bucketName in this.buckets) {
158-
return true
159-
}
160-
return false
161-
}
162-
}
163-
16427
export class AssetProxy {
16528
/**
16629
* @param {AssetBuckets} buckets - AssetBuckets instance
@@ -170,7 +33,7 @@ export class AssetProxy {
17033
constructor(buckets, urlExpiry, bucketOption) {
17134
this.buckets = buckets
17235
this.urlExpiry = urlExpiry
173-
this.isEnabled = bucketOption !== BucketOption.NONE
36+
this.isEnabled = bucketOption !== BucketOptionEnum.NONE
17437
}
17538

17639
/**
@@ -182,7 +45,7 @@ export class AssetProxy {
18245
const bucketList = process.env['ASSET_PROXY_BUCKET_LIST']
18346

18447
let bucketNames = null
185-
if (bucketOption === BucketOption.LIST) {
48+
if (bucketOption === BucketOptionEnum.LIST) {
18649
if (!bucketList) {
18750
throw new Error(
18851
'ASSET_PROXY_BUCKET_LIST must be set when ASSET_PROXY_BUCKET_OPTION is LIST'

0 commit comments

Comments
 (0)