Skip to content

Commit 840cac1

Browse files
authored
StaticSite: support removing old files (#1541)
* StaticSite: support removing old files * Sync
1 parent 9ca28b2 commit 840cac1

File tree

4 files changed

+93
-10
lines changed

4 files changed

+93
-10
lines changed

packages/resources/assets/BaseSite/archiver.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,9 @@ function generateZips() {
107107
}
108108

109109
await archive.finalize();
110+
111+
// Create a filenames file
112+
const filenamesPath = path.join(ZIP_PATH, `filenames`);
113+
fs.writeFileSync(filenamesPath, files.join("\n"));
110114
});
111115
}

packages/resources/assets/BaseSite/custom-resource/s3-handler.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def cfn_error(message=None):
5050
try:
5151
sources = props['Sources']
5252
dest_bucket_name = props['DestinationBucketName']
53+
filenames = props.get('Filenames', None)
5354
file_options = props.get('FileOptions', [])
5455
replace_values = props.get('ReplaceValues', [])
5556
except KeyError as e:
@@ -69,6 +70,9 @@ def cfn_error(message=None):
6970
if request_type == "Update" or request_type == "Create":
7071
loop = asyncio.get_event_loop()
7172
loop.run_until_complete(s3_deploy_all(sources, dest_bucket_name, file_options, replace_values))
73+
# purge old items
74+
if filenames:
75+
s3_purge(filenames, dest_bucket_name)
7276

7377
cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id)
7478
except KeyError as e:
@@ -124,6 +128,32 @@ def s3_deploy(function_name, source, dest_bucket_name, file_options, replace_val
124128
if (result['Status'] != True):
125129
raise Exception("failed to upload to s3")
126130

131+
#---------------------------------------------------------------------------------------------------
132+
# remove old files
133+
def s3_purge(filenames, dest_bucket_name):
134+
logger.info("| s3_purge")
135+
136+
source_bucket_name = filenames['BucketName']
137+
source_object_key = filenames['ObjectKey']
138+
s3_source = "s3://%s/%s" % (source_bucket_name, source_object_key)
139+
140+
# create a temporary working directory
141+
workdir=tempfile.mkdtemp()
142+
logger.info("| workdir: %s" % workdir)
143+
144+
# download the archive from the source and extract to "contents"
145+
target_path=os.path.join(workdir, str(uuid4()))
146+
logger.info("target_path: %s" % target_path)
147+
aws_command("s3", "cp", s3_source, target_path)
148+
with open(target_path) as f:
149+
filepaths = f.read().splitlines()
150+
151+
#s3_dest get S3 files
152+
for file in s3.Bucket(dest_bucket_name).objects.all():
153+
if (file.key not in filepaths):
154+
logger.info("| removing file %s", file.key)
155+
file.delete()
156+
127157
#---------------------------------------------------------------------------------------------------
128158
# executes an "aws" cli command
129159
def aws_command(*args):

packages/resources/src/StaticSite.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface StaticSiteProps {
4646
readonly s3Bucket?: s3.BucketProps;
4747
readonly cfDistribution?: StaticSiteCdkDistributionProps;
4848
readonly environment?: { [key: string]: string };
49+
readonly purgeFiles?: boolean;
4950
readonly disablePlaceholder?: boolean;
5051
readonly waitForInvalidation?: boolean;
5152
}
@@ -68,6 +69,7 @@ export class StaticSite extends Construct implements SSTConstruct {
6869
private readonly props: StaticSiteProps;
6970
private readonly isPlaceholder: boolean;
7071
private readonly assets: s3Assets.Asset[];
72+
private readonly filenamesAsset?: s3Assets.Asset;
7173
private readonly awsCliLayer: AwsCliLayer;
7274

7375
constructor(scope: Construct, id: string, props: StaticSiteProps) {
@@ -92,7 +94,9 @@ export class StaticSite extends Construct implements SSTConstruct {
9294
this.validateCustomDomainSettings();
9395

9496
// Build app
95-
this.assets = this.buildApp(fileSizeLimit, buildDir);
97+
this.buildApp();
98+
this.assets = this.bundleAssets(fileSizeLimit, buildDir);
99+
this.filenamesAsset = this.bundleFilenamesAsset(buildDir);
96100

97101
// Create Bucket
98102
this.s3Bucket = this.createS3Bucket();
@@ -159,17 +163,12 @@ export class StaticSite extends Construct implements SSTConstruct {
159163
};
160164
}
161165

162-
private buildApp(fileSizeLimit: number, buildDir: string): s3Assets.Asset[] {
166+
private buildApp() {
163167
if (this.isPlaceholder) {
164-
return [
165-
new s3Assets.Asset(this, "Asset", {
166-
path: path.resolve(__dirname, "../assets/StaticSite/stub"),
167-
}),
168-
];
168+
return;
169169
}
170170

171171
const { path: sitePath, buildCommand } = this.props;
172-
const buildOutput = this.props.buildOutput || ".";
173172

174173
// validate site path exists
175174
if (!fs.existsSync(sitePath)) {
@@ -180,8 +179,6 @@ export class StaticSite extends Construct implements SSTConstruct {
180179
);
181180
}
182181

183-
// Build and package user's website
184-
185182
// build
186183
if (buildCommand) {
187184
try {
@@ -200,6 +197,22 @@ export class StaticSite extends Construct implements SSTConstruct {
200197
);
201198
}
202199
}
200+
}
201+
202+
private bundleAssets(
203+
fileSizeLimit: number,
204+
buildDir: string
205+
): s3Assets.Asset[] {
206+
if (this.isPlaceholder) {
207+
return [
208+
new s3Assets.Asset(this, "Asset", {
209+
path: path.resolve(__dirname, "../assets/StaticSite/stub"),
210+
}),
211+
];
212+
}
213+
214+
const { path: sitePath, buildCommand } = this.props;
215+
const buildOutput = this.props.buildOutput || ".";
203216

204217
// validate buildOutput exists
205218
const siteOutputPath = path.resolve(path.join(sitePath, buildOutput));
@@ -248,6 +261,31 @@ export class StaticSite extends Construct implements SSTConstruct {
248261
return assets;
249262
}
250263

264+
private bundleFilenamesAsset(buildDir: string): s3Assets.Asset | undefined {
265+
if (this.isPlaceholder) {
266+
return;
267+
}
268+
if (this.props.purgeFiles === false) {
269+
return;
270+
}
271+
272+
const zipPath = path.resolve(
273+
path.join(buildDir, `StaticSite-${this.node.id}-${this.node.addr}`)
274+
);
275+
276+
// create assets
277+
const filenamesPath = path.join(zipPath, `filenames`);
278+
if (!fs.existsSync(filenamesPath)) {
279+
throw new Error(
280+
`There was a problem generating the "${this.node.id}" StaticSite package.`
281+
);
282+
}
283+
284+
return new s3Assets.Asset(this, `AssetFilenames`, {
285+
path: filenamesPath,
286+
});
287+
}
288+
251289
private createS3Bucket(): s3.Bucket {
252290
let { s3Bucket } = this.props;
253291
s3Bucket = s3Bucket || {};
@@ -304,6 +342,7 @@ export class StaticSite extends Construct implements SSTConstruct {
304342
},
305343
});
306344
this.s3Bucket.grantReadWrite(handler);
345+
this.filenamesAsset?.grantRead(handler);
307346
uploader.grantInvoke(handler);
308347

309348
// Create custom resource
@@ -316,6 +355,10 @@ export class StaticSite extends Construct implements SSTConstruct {
316355
ObjectKey: asset.s3ObjectKey,
317356
})),
318357
DestinationBucketName: this.s3Bucket.bucketName,
358+
Filenames: this.filenamesAsset && {
359+
BucketName: this.filenamesAsset.s3BucketName,
360+
ObjectKey: this.filenamesAsset.s3ObjectKey,
361+
},
319362
FileOptions: (fileOptions || []).map(
320363
({ exclude, include, cacheControl }) => {
321364
if (typeof exclude === "string") {

www/docs/constructs/StaticSite.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,12 @@ _Type_: [`StaticSiteCdkDistributionProps`](#staticsitecdkdistributionprops)
639639

640640
Pass in a `StaticSiteCdkDistributionProps` value to override the default settings this construct uses to create the CDK `Distribution` internally.
641641

642+
### purgeFiles?
643+
644+
_Type_ : `boolean`, _defaults to true_
645+
646+
While deploying, SST removes old files that no longer exist. Pass in `false` to keep the old files around.
647+
642648
### waitForInvalidation?
643649

644650
_Type_ : `boolean`, _defaults to true_

0 commit comments

Comments
 (0)