diff --git a/.gitignore b/.gitignore index 034b038..9fd7ff4 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,7 @@ typings/ # Ignore Terraform stuff in examples .terraform *.tfstate -*.tfstate.backup \ No newline at end of file +*.tfstate.backup + +# WebStorm +.idea diff --git a/src/FileItem.ts b/src/FileItem.ts index 651709a..6d7ce2a 100644 --- a/src/FileItem.ts +++ b/src/FileItem.ts @@ -1,9 +1,11 @@ import * as Path from 'path' import * as fs from 'fs-extra' import { Item, Container } from './types' +import * as crypto from 'crypto' export default class FileItem implements Item { private baseDirectory: string + private computedEtag?: string public key: string public modtime: Date @@ -21,6 +23,30 @@ export default class FileItem implements Item { this.isSymbolicLink = stats.isSymbolicLink() } + public etag(): string { + if (this.computedEtag === undefined) { + this.computedEtag = FileItem.computeEtag(Path.join(this.baseDirectory, this.key)) + } + return this.computedEtag! + } + + private static computeEtag(filepath: string): string { + const hash = crypto.createHash('md5') + const buffer = Buffer.alloc(8192) + + const fd = fs.openSync(filepath, 'r') + try { + let bytesRead + do { + bytesRead = fs.readSync(fd, buffer, 0, 8192, null) + hash.update(buffer.slice(0, bytesRead)) + } while (bytesRead === 8192) + } finally { + fs.closeSync(fd) + } + return `"${hash.digest('hex')}"` + } + read(): Promise { return fs.readFile(Path.join(this.baseDirectory, this.key)) } diff --git a/src/S3Container.ts b/src/S3Container.ts index 076d62d..c54515d 100644 --- a/src/S3Container.ts +++ b/src/S3Container.ts @@ -93,6 +93,7 @@ export default class S3Container extends S3Prefixer implements Container { const s3Object: S3.Object = { Key: key, LastModified: objectHead.LastModified, + ETag: objectHead.ETag, Size: body.length, } const ret: Item = new S3Item(item.key, s3Object) diff --git a/src/S3Item.ts b/src/S3Item.ts index 41c06b1..880084d 100644 --- a/src/S3Item.ts +++ b/src/S3Item.ts @@ -7,6 +7,7 @@ const s3 = new S3() export default class S3Item extends S3Prefixer implements Item { private s3Object: S3.Object private bucketName: string + private remoteEtag?: string public key: string public modtime: Date @@ -30,6 +31,7 @@ export default class S3Item extends S3Prefixer implements Item { } this.key = s3Object.Key this.modtime = s3Object.LastModified + this.remoteEtag = s3Object.ETag this.size = s3Object.Size this.isSymbolicLink = false } @@ -59,4 +61,8 @@ export default class S3Item extends S3Prefixer implements Item { public read(): Promise { return this.getBody() } + + public etag(): string { + return this.remoteEtag || '' + } } \ No newline at end of file diff --git a/src/Stats.ts b/src/Stats.ts index 8ddcb1e..bdc5697 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -18,8 +18,8 @@ export interface StatsObject { } export default class Stats implements StatsObject { - public source: string - public target: string + public source: string = '' + public target: string = '' public diffs: ItemDiff[] = [] public distributions?: CloudFront.DistributionSummary[] public invalidations?: string[] diff --git a/src/Websync.ts b/src/Websync.ts index 6d2096a..30dc2e7 100644 --- a/src/Websync.ts +++ b/src/Websync.ts @@ -77,12 +77,12 @@ export default class Websync extends EventEmitter implements WebsyncEmitter { private initialized: boolean = false private completed: boolean = false - private diffs: ItemDiff[] + private diffs: ItemDiff[] = [] private stats: Stats - private transfer: Transfer + private transfer?: Transfer private invalidations: string[] | undefined private invalidator: CloudFrontInvalidator | undefined - private distributions: string[] + private distributions?: string[] private completeCount = 0 @@ -197,7 +197,9 @@ export default class Websync extends EventEmitter implements WebsyncEmitter { } try { - await this.transfer.complete() + if (this.transfer !== undefined) { + await this.transfer.complete() + } } catch (err) { throw new Errors.TransferFailed(err) } diff --git a/src/cli.ts b/src/cli.ts index 51030e5..f517f80 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,7 +16,7 @@ Options: --config Provide a configuration file (js or json) --include Pattern to include --exclude Patter to exclude - --diffBy Property by which objects are diffed (modtime or size), default is modtime + --diffBy Property by which objects are diffed (etag, modtime or size), default is modtime --wildcardPolicy Set the wildcard policy (majority, minority, or unanmious) --wildcardAll Append wildcard to all invalidations --invalidateDeletes Invalidate delete transfers diff --git a/src/diff.ts b/src/diff.ts index 16582be..6fb7356 100644 --- a/src/diff.ts +++ b/src/diff.ts @@ -1,6 +1,6 @@ import { Container, Item, ItemDiffType, ItemDiff, FilterOptions } from './types' -export type DiffKey = 'modtime' | 'size' +export type DiffKey = 'modtime' | 'size' | 'etag' const getItem = (items: Item[], key: string): Item | undefined => { for (let i = 0; i < items.length; i++) { @@ -14,6 +14,8 @@ const shouldUpdate = (source: Item, target: Item, diffKey: DiffKey): boolean => return source.modtime.getTime() > target.modtime.getTime() case 'size': return source.size !== target.size + case 'etag': + return source.etag() !== target.etag() } } diff --git a/src/types.ts b/src/types.ts index 2e1ebbf..cc13dc5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,7 @@ export interface Item { modtime: Date size: number isSymbolicLink: boolean + etag(): string read(): Promise } diff --git a/test/unit/S3Container.test.ts b/test/unit/S3Container.test.ts index d1e8007..cb196e1 100644 --- a/test/unit/S3Container.test.ts +++ b/test/unit/S3Container.test.ts @@ -64,6 +64,7 @@ const createContainerTest = (prefix: string = '') => () => { const item: Item = { key: 'bang/a/b/d.txt', modtime: new Date(), + etag: () => '3e25960a79dbc69b674cd4ec67a72c62', size: body.length, isSymbolicLink: false, read: () => Promise.resolve(body), @@ -89,6 +90,7 @@ const createContainerTest = (prefix: string = '') => () => { const item: Item = { key: 'test.txt', modtime: new Date(), + etag: () => '3e25960a79dbc69b674cd4ec67a72c62', size: body.length, isSymbolicLink: false, read: () => Promise.resolve(body), @@ -114,6 +116,7 @@ const createContainerTest = (prefix: string = '') => () => { const item: Item = { key: 'index.html', modtime: new Date(), + etag: () => '3e25960a79dbc69b674cd4ec67a72c62', size: body.length, isSymbolicLink: false, read: () => Promise.resolve(body), diff --git a/test/utils.ts b/test/utils.ts index d377163..beaf0f7 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -20,6 +20,10 @@ export class MockItem implements Item { public read(): Promise { return Promise.resolve(new Buffer(this.size)) } + + etag(): string { + return faker.random.word() + } } const randomContainerType = (): ContainerType =>