import { S3 } from 'aws-sdk' ; import { expireTagKey , expireTagValue } from './constants' ; import { ExpireValue , FileResult , Manifest , ManifestFile } from './types' ; const dynamicPartMatcher = new RegExp ( /\/?\[[^\]]+\].*/ ) ; async function expireFiles ( s3 , bucketId , files ) { for ( const file of files ) { await s3 . copyObject ( { Bucket : bucketId , CopySource : `/${ bucketId }/${ file }` , Key : file , StorageClass : 'STANDARD' , } ) . promise ( ) ; const existingTags = await s3 . getObjectTagging ( { Bucket : bucketId , Key : file , } ) . promise ( ) ; await s3 . putObjectTagging ( { Bucket : bucketId , Key : file , Tagging : { TagSet : [ ... existingTags . TagSet , { Key : expireTagKey , Value : expireTagValue , } , ] , } , } ) . promise ( ) ; } } async function removeExpirationFromFiles ( s3 , bucketId , files ) { for ( const file of files ) { const tags = await s3 . getObjectTagging ( { Bucket : bucketId , Key : file , } ) . promise ( ) ; const newTags = tags . TagSet . filter ( ( { Key } ) => { return Key !== expireTagKey ; } ) ; await s3 . putObjectTagging ( { Bucket : bucketId , Key : file , Tagging : { TagSet : newTags , } , } ) ; } } async function deleteFiles ( s3 , bucketId , files ) { try { await s3 . deleteObjects ( { Bucket : bucketId , Delete : { Objects : files . map ( ( file ) => ( { Key : file , } ) ) , } , } ) . promise ; } catch ( err ) { } } function getInvalidationKeys ( files ) { const invalidations = [ ] ; for ( const file of files ) { if ( file . startsWith ( '_next' ) ) { continue ; } if ( file === 'index' ) { invalidations . push ( '/' , '/?*' ) ; continue ; } if ( file . match ( dynamicPartMatcher ) ) { invalidations . push ( `/${ file . replace ( dynamicPartMatcher , '*' ) }` ) ; continue ; } if ( file . endsWith ( '/index' ) ) { invalidations . push ( `/${ file . slice ( 0 , - 6 ) }*` ) ; continue ; } invalidations . push ( `/${ file }*` ) ; } return invalidations ; } interface Props { s3 : S3 ; bucket : string ; buildId : string ; files : FileResult [ ] ; expireAfterDays : ExpireValue ; deploymentConfigurationKey : string ; manifest : Manifest ; } interface Response { expire : string [ ] ; restore : string [ ] ; deleted : string [ ] ; invalidate : string [ ] ; manifest : Manifest ; } /**
 * Reads or creates a deployment.json file which holds information about
 * which files were included in the deployment
 *
 * It returns an array of keys that should be expired
 */ async function updateManifest ( { files , buildId , s3 , deploymentConfigurationKey , expireAfterDays , bucket , manifest , } ) { const expire = [ ] ; const deleted = [ ] ; const restore = [ ] ; const unchangedFiles = new Set < string > ( ) ; const newManifestFiles = { } ; const { files : manifestFiles } = manifest ; const now = new Date ( ) ; const minExpireDate = expireAfterDays !== 'never' ? new Date ( now . getDate ( ) - expireAfterDays ) : now ; const fileKeys = files . map ( ( { key } ) => key ) ; const allFiles = new Set ( [ ... fileKeys , ... Object . keys ( manifestFiles ) ] ) ; for ( const fileKey of allFiles ) { if ( ! ( fileKey in manifestFiles ) ) { const file = files . find ( ( { key } ) => key === fileKey ) ; newManifestFiles [ fileKey ] = { buildId : [ buildId ] , eTag : file ?. eTag , } ; continue ; } const file = manifestFiles [ fileKey ] ; let isDeleted = false ; const changedFile = files . find ( ( { key } ) => key === fileKey ) ; if ( changedFile === undefined ) { if ( ! file . expiredAt ) { if ( fileKey . startsWith ( '_next' ) ) { expire . push ( fileKey ) ; file . expiredAt = now . toUTCString ( ) ; } else { deleted . push ( fileKey ) ; isDeleted = true ; } } } else { file . buildId . push ( buildId ) ; if ( file . eTag === changedFile . eTag ) { unchangedFiles . add ( fileKey ) ; } else { file . eTag = changedFile . eTag ; } if ( file . expiredAt ) { restore . push ( fileKey ) ; delete file . expiredAt ; } } if ( expireAfterDays !== 'never' && file . expiredAt ) { const fileExpirationDate = new Date ( file . expiredAt ) ; if ( fileExpirationDate > minExpireDate ) { newManifestFiles [ fileKey ] = file ; } } else { if ( ! isDeleted ) { newManifestFiles [ fileKey ] = file ; } } } const newManifest = { ... manifest , currentBuild : buildId , files : newManifestFiles , } ; await expireFiles ( s3 , bucket , expire ) ; await removeExpirationFromFiles ( s3 , bucket , restore ) ; await deleteFiles ( s3 , bucket , deleted ) ; const changedFiles = Array . from ( allFiles ) . filter ( ( fileKey ) => ! unchangedFiles . has ( fileKey ) ) ; const invalidate = getInvalidationKeys ( changedFiles ) ; await s3 . putObject ( { Bucket : bucket , Key : deploymentConfigurationKey , Body : JSON . stringify ( newManifest ) , } ) . promise ( ) ; return { expire , restore , manifest : newManifest , invalidate , deleted } ; } export { updateManifest , getInvalidationKeys } ;