Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent deleting excluded objects by default #50

Merged
merged 2 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to this project will be documented in this file.

## [4.2.0]

### Bug Fixes

* prevent deleting excluded files by default to match aws cli behavior

### Features

* add a `deleteExcluded` option to delete excluded files

## [4.1.0]

### Features
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ Similar to AWS CLI ``aws s3 sync localDir bucketPrefix [options]``.
- `options` *<SyncBucketWithLocalOptions\>*
- `dryRun` *<boolean\>* Equivalent to CLI ``--dryrun`` option
- `del` *<boolean\>* Equivalent to CLI ``--delete`` option
- `deleteExcluded` *<boolean\>* Delete **excluded** target objects even if they match a source object. Ignored if `del` is false. See [this](https://github.com/aws/aws-cli/issues/4923) CLI issue.
- `sizeOnly` *<boolean\>* Equivalent to CLI ``--size-only`` option
- `relocations` *<Relocation[]\>* Allows uploading objects to remote folders without mirroring the source directory structure. Each relocation is as a callback taking a string posix path param and returning a relocated string posix path.
- `filters` *<Filter[]\>* [Almost](https://github.com/jeanbmar/s3-sync-client/issues/30) equivalent to CLI ``--exclude`` and ``--include`` options. Filters can be specified using plain objects including either an `include` or `exclude` property. The `include` and `exclude` properties are functions that take an object key and return a boolean.
Expand All @@ -243,6 +244,7 @@ Similar to AWS CLI ``aws s3 sync bucketPrefix localDir [options]``.
- `options` *<SyncLocalWithBucketOptions\>*
- `dryRun` *<boolean\>* Equivalent to CLI ``--dryrun`` option
- `del` *<boolean\>* Equivalent to CLI ``--delete`` option
- `deleteExcluded` *<boolean\>* Delete **excluded** target objects even if they match a source object. Ignored if `del` is false. See [this](https://github.com/aws/aws-cli/issues/4923) CLI issue.
- `sizeOnly` *<boolean\>* Equivalent to CLI ``--size-only`` option
- `relocations` *<Relocation[]\>* Allows downloading objects to local directories without mirroring the source folder structure. Each relocation is as a callback taking a string posix path param and returning a relocated string posix path.
- `filters` *<Filter[]\>* [Almost](https://github.com/jeanbmar/s3-sync-client/issues/30) equivalent to CLI ``--exclude`` and ``--include`` options. Filters can be specified using plain objects including either an `include` or `exclude` property. The `include` and `exclude` properties are functions that take an object key and return a boolean.
Expand All @@ -265,6 +267,7 @@ Similar to AWS CLI ``aws s3 sync sourceBucketPrefix targetBucketPrefix [options]
- `options` *<SyncBucketWithBucketOptions\>*
- `dryRun` *<boolean\>* Equivalent to CLI ``--dryrun`` option
- `del` *<boolean\>* Equivalent to CLI ``--delete`` option
- `deleteExcluded` *<boolean\>* Delete **excluded** target objects even if they match a source object. Ignored if `del` is false. See [this](https://github.com/aws/aws-cli/issues/4923) CLI issue.
- `sizeOnly` *<boolean\>* Equivalent to CLI ``--size-only`` option
- `relocations` *<Relocation[]\>* Allows copying objects to remote folders without mirroring the source folder structure. Each relocation is as a callback taking a string posix path param and returning a relocated string posix path.
- `filters` *<Filter[]\>* [Almost](https://github.com/jeanbmar/s3-sync-client/issues/30) equivalent to CLI ``--exclude`` and ``--include`` options. Filters can be specified using plain objects including either an `include` or `exclude` property. The `include` and `exclude` properties are functions that take an object key and return a boolean.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "s3-sync-client",
"version": "4.1.2",
"version": "4.2.0",
"description": "AWS CLI s3 sync for Node.js provides a modern client to perform S3 sync operations between file systems and S3 buckets in the spirit of the official AWS CLI command",
"keywords": [
"aws",
Expand Down
25 changes: 11 additions & 14 deletions src/commands/SyncBucketWithBucketCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type SyncBucketWithBucketCommandInput = {
targetBucketPrefix: string;
dryRun?: boolean;
del?: boolean;
deleteExcluded?: boolean;
sizeOnly?: boolean;
relocations?: Relocation[];
filters?: Filter[];
Expand All @@ -35,6 +36,7 @@ export class SyncBucketWithBucketCommand {
targetBucketPrefix: string;
dryRun: boolean;
del: boolean;
deleteExcluded: boolean;
sizeOnly: boolean;
relocations: Relocation[];
filters: Filter[];
Expand All @@ -48,6 +50,7 @@ export class SyncBucketWithBucketCommand {
this.targetBucketPrefix = input.targetBucketPrefix;
this.dryRun = input.dryRun ?? false;
this.del = input.del ?? false;
this.deleteExcluded = input.deleteExcluded ?? false;
this.sizeOnly = input.sizeOnly ?? false;
this.relocations = input.relocations ?? [];
this.filters = input.filters ?? [];
Expand Down Expand Up @@ -88,20 +91,14 @@ export class SyncBucketWithBucketCommand {
: currentPath,
...this.relocations,
];
sourceObjects.forEach((sourceObject) =>
sourceObject.applyFilters(this.filters)
);
const includedSourceObjects = sourceObjects.filter(
(sourceObject) => sourceObject.isIncluded
);
includedSourceObjects.forEach((sourceObject) =>
sourceObject.applyRelocations(this.relocations)
);
const diff = SyncObject.diff(
includedSourceObjects,
targetObjects,
this.sizeOnly
);
sourceObjects.forEach((sourceObject) => {
sourceObject.applyFilters(this.filters);
sourceObject.applyRelocations(this.relocations);
});
const diff = SyncObject.diff(sourceObjects, targetObjects, {
sizeOnly: this.sizeOnly,
deleteExcluded: this.deleteExcluded,
});
const commands = [];
if (!this.dryRun) {
commands.push(
Expand Down
25 changes: 11 additions & 14 deletions src/commands/SyncBucketWithLocalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type SyncBucketWithLocalCommandInput = {
bucketPrefix: string;
dryRun?: boolean;
del?: boolean;
deleteExcluded?: boolean;
sizeOnly?: boolean;
relocations?: Relocation[];
filters?: Filter[];
Expand All @@ -47,6 +48,7 @@ export class SyncBucketWithLocalCommand {
bucketPrefix: string;
dryRun: boolean;
del: boolean;
deleteExcluded: boolean;
sizeOnly: boolean;
relocations: Relocation[];
filters: Filter[];
Expand All @@ -63,6 +65,7 @@ export class SyncBucketWithLocalCommand {
this.bucketPrefix = input.bucketPrefix;
this.dryRun = input.dryRun ?? false;
this.del = input.del ?? false;
this.deleteExcluded = input.deleteExcluded ?? false;
this.sizeOnly = input.sizeOnly ?? false;
this.relocations = input.relocations ?? [];
this.filters = input.filters ?? [];
Expand All @@ -85,20 +88,14 @@ export class SyncBucketWithLocalCommand {
(currentPath) => `${prefix}/${currentPath}`,
...this.relocations,
];
sourceObjects.forEach((sourceObject) =>
sourceObject.applyFilters(this.filters)
);
const includedSourceObjects = sourceObjects.filter(
(sourceObject) => sourceObject.isIncluded
);
includedSourceObjects.forEach((sourceObject) =>
sourceObject.applyRelocations(this.relocations)
);
const diff = SyncObject.diff(
includedSourceObjects,
targetObjects,
this.sizeOnly
);
sourceObjects.forEach((sourceObject) => {
sourceObject.applyFilters(this.filters);
sourceObject.applyRelocations(this.relocations);
});
const diff = SyncObject.diff(sourceObjects, targetObjects, {
sizeOnly: this.sizeOnly,
deleteExcluded: this.deleteExcluded,
});
const commands = [];
if (!this.dryRun) {
commands.push(
Expand Down
21 changes: 9 additions & 12 deletions src/commands/SyncLocalWithBucketCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type SyncLocalWithBucketCommandInput = {
localDir: string;
dryRun?: boolean;
del?: boolean;
deleteExcluded?: boolean;
sizeOnly?: boolean;
relocations?: Relocation[];
filters?: Filter[];
Expand All @@ -37,6 +38,7 @@ export class SyncLocalWithBucketCommand {
localDir: string;
dryRun: boolean;
del: boolean;
deleteExcluded: boolean;
sizeOnly: boolean;
relocations: Relocation[];
filters: Filter[];
Expand All @@ -50,6 +52,7 @@ export class SyncLocalWithBucketCommand {
this.localDir = input.localDir;
this.dryRun = input.dryRun ?? false;
this.del = input.del ?? false;
this.deleteExcluded = input.deleteExcluded ?? false;
this.sizeOnly = input.sizeOnly ?? false;
this.relocations = input.relocations ?? [];
this.filters = input.filters ?? [];
Expand All @@ -75,20 +78,14 @@ export class SyncLocalWithBucketCommand {
: currentPath,
...this.relocations,
];
sourceObjects.forEach((sourceObject) =>
sourceObject.applyFilters(this.filters)
);
const includedSourceObjects = sourceObjects.filter(
(sourceObject) => sourceObject.isIncluded
);
includedSourceObjects.forEach((sourceObject) => {
sourceObjects.forEach((sourceObject) => {
sourceObject.applyFilters(this.filters);
sourceObject.applyRelocations(this.relocations);
});
const diff = SyncObject.diff(
includedSourceObjects,
targetObjects,
this.sizeOnly
);
const diff = SyncObject.diff(sourceObjects, targetObjects, {
sizeOnly: this.sizeOnly,
deleteExcluded: this.deleteExcluded,
});
const commands = [];
if (!this.dryRun) {
commands.push(
Expand Down
17 changes: 14 additions & 3 deletions src/fs/SyncObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export type Diff = {
deleted: SyncObject[];
};

export type DiffOptions = {
sizeOnly?: boolean;
deleteExcluded?: boolean;
};

export abstract class SyncObject {
id: string;
size: number;
Expand All @@ -27,7 +32,7 @@ export abstract class SyncObject {
static diff(
sourceObjects: SyncObject[],
targetObjects: SyncObject[],
sizeOnly: boolean = false
options?: DiffOptions
): Diff {
const sourceObjectMap = new Map(
sourceObjects.map((sourceObject) => [sourceObject.id, sourceObject])
Expand All @@ -38,19 +43,25 @@ export abstract class SyncObject {
const created = [];
const updated = [];
sourceObjectMap.forEach((sourceObject) => {
if (!sourceObject.isIncluded) return;
const targetObject = targetObjectMap.get(sourceObject.id);
if (targetObject === undefined) {
created.push(sourceObject);
} else if (
sourceObject.size !== targetObject.size ||
(!sizeOnly && sourceObject.lastModified > targetObject.lastModified)
(options?.sizeOnly !== true &&
sourceObject.lastModified > targetObject.lastModified)
) {
updated.push(sourceObject);
}
});
const deleted = [];
targetObjectMap.forEach((targetObject) => {
if (!sourceObjectMap.has(targetObject.id)) {
const sourceObject = sourceObjectMap.get(targetObject.id);
if (
sourceObject === undefined ||
(!sourceObject.isIncluded && options?.deleteExcluded === true)
) {
deleted.push(targetObject);
}
});
Expand Down
72 changes: 55 additions & 17 deletions test/S3SyncClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,42 @@ test('s3 sync client', async (t) => {
assert(hasObject(objects, 'xmoj') === true);
});

// https://github.com/jeanbmar/s3-sync-client/issues/30
await b.test('does not delete excluded files', async () => {
await syncClient.send(
new SyncBucketWithLocalCommand({
localDir: path.join(DATA_DIR, 'def/jkl'),
bucketPrefix: BUCKET,
del: true,
filters: [{ exclude: (key) => key.startsWith('xmot') }],
})
);
const objects = await syncClient.send(
new ListBucketObjectsCommand({
bucket: BUCKET,
})
);
assert(hasObject(objects, 'xmot') === true);
});

await b.test('deletes excluded files', async () => {
await syncClient.send(
new SyncBucketWithLocalCommand({
localDir: path.join(DATA_DIR, 'def/jkl'),
bucketPrefix: BUCKET,
del: true,
deleteExcluded: true,
filters: [{ exclude: (key) => key.startsWith('xmot') }],
})
);
const objects = await syncClient.send(
new ListBucketObjectsCommand({
bucket: BUCKET,
})
);
assert(hasObject(objects, 'xmot') === false);
});

await b.test(
'syncs a single dir with a bucket using relocation',
async () => {
Expand Down Expand Up @@ -588,43 +624,44 @@ test('s3 sync client', async (t) => {

await t.test('diffs sync objects', async (d) => {
const bucketObjects = [
{ id: 'abc/created', lastModified: 0, size: 1 },
{ id: 'abc/updated1', lastModified: 1, size: 1 },
{ id: 'abc/updated2', lastModified: 0, size: 2 },
{ id: 'abc/unchanged', lastModified: 0, size: 1 },
{ id: 'abc/created', lastModified: 0, size: 1, isIncluded: true },
{ id: 'abc/updated1', lastModified: 1, size: 1, isIncluded: true },
{ id: 'abc/updated2', lastModified: 0, size: 2, isIncluded: true },
{ id: 'abc/unchanged', lastModified: 0, size: 1, isIncluded: true },
] as BucketObject[];
const localObjects = [
{ id: 'abc/unchanged', lastModified: 0, size: 1 },
{ id: 'abc/updated1', lastModified: 0, size: 1 },
{ id: 'abc/updated2', lastModified: 0, size: 1 },
{ id: 'deleted', lastModified: 0, size: 1 },
{ id: 'abc/unchanged', lastModified: 0, size: 1, isIncluded: true },
{ id: 'abc/updated1', lastModified: 0, size: 1, isIncluded: true },
{ id: 'abc/updated2', lastModified: 0, size: 1, isIncluded: true },
{ id: 'deleted', lastModified: 0, size: 1, isIncluded: true },
] as LocalObject[];

await d.test('computes sync operations on objects', () => {
const diff = SyncObject.diff(bucketObjects, localObjects);
assert.deepStrictEqual(diff.created, [
{ id: 'abc/created', size: 1, lastModified: 0 },
{ id: 'abc/created', size: 1, lastModified: 0, isIncluded: true },
]);
assert.deepStrictEqual(diff.updated, [
{ id: 'abc/updated1', size: 1, lastModified: 1 },
{ id: 'abc/updated2', size: 2, lastModified: 0 },
{ id: 'abc/updated1', size: 1, lastModified: 1, isIncluded: true },
{ id: 'abc/updated2', size: 2, lastModified: 0, isIncluded: true },
]);
assert.deepStrictEqual(diff.deleted, [
{ id: 'deleted', size: 1, lastModified: 0 },
{ id: 'deleted', size: 1, lastModified: 0, isIncluded: true },
]);
});

await d.test('computes sync sizeOnly operations on objects', () => {
const sizeOnly = true;
const diff = SyncObject.diff(bucketObjects, localObjects, sizeOnly);
const diff = SyncObject.diff(bucketObjects, localObjects, {
sizeOnly: true,
});
assert.deepStrictEqual(diff.created, [
{ id: 'abc/created', size: 1, lastModified: 0 },
{ id: 'abc/created', size: 1, lastModified: 0, isIncluded: true },
]);
assert.deepStrictEqual(diff.updated, [
{ id: 'abc/updated2', size: 2, lastModified: 0 },
{ id: 'abc/updated2', size: 2, lastModified: 0, isIncluded: true },
]);
assert.deepStrictEqual(diff.deleted, [
{ id: 'deleted', size: 1, lastModified: 0 },
{ id: 'deleted', size: 1, lastModified: 0, isIncluded: true },
]);
});
});
Expand Down Expand Up @@ -671,6 +708,7 @@ test('s3 sync client', async (t) => {
{ include: (key) => key.startsWith('def/jkl') },
],
del: true,
deleteExcluded: true,
})
);
const objects = await syncClient.send(
Expand Down