@kensio/yulin 1.21.31 → 1.21.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/serve/message/sim-message-log-console.js +3 -3
  2. package/dist/service/apigateway/api/deployment/sim-rest-api-deployment.js +3 -3
  3. package/dist/service/apigateway/api/method/sim-rest-api-method.js +12 -12
  4. package/dist/service/apigateway/api/resource/sim-rest-api-resource.js +11 -9
  5. package/dist/service/apigateway/api/sim-rest-api-view.js +3 -3
  6. package/dist/service/apigateway/api/stage/sim-rest-api-stage.js +8 -10
  7. package/dist/service/apigatewayv2/api/integration/sim-http-api-integration.js +3 -3
  8. package/dist/service/apigatewayv2/api/route/sim-http-api-route-view.js +6 -6
  9. package/dist/service/apigatewayv2/api/sim-http-api-view.js +3 -3
  10. package/dist/service/apigatewayv2/api/stage/settings/sim-http-api-stage-route-settings.js +8 -7
  11. package/dist/service/apigatewayv2/api/stage/sim-http-api-stage.js +9 -9
  12. package/dist/service/athena/engine/sim-athena-partitioned-objects.js +7 -7
  13. package/dist/service/athena/sim-athena-engine.fixture.js +3 -0
  14. package/dist/service/athena/table/sim-athena-partition-filters.js +6 -5
  15. package/dist/service/backup/command/sim-backup-authorizer.js +5 -4
  16. package/dist/service/bedrock/response/sim-bedrock-declared-content.js +5 -10
  17. package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-changes.js +19 -10
  18. package/dist/service/cloudfront/cfn/response-headers-policy/sim-cfn-cf-rh-policy-security-values.js +2 -6
  19. package/dist/service/cognito/user-pool/trigger/sim-cognito-pre-sign-up-response.js +4 -7
  20. package/dist/service/dynamodb/command/transact/sim-dynamodb-transact-write.js +34 -25
  21. package/dist/service/dynamodb/secondary-index/sim-dynamodb-index-limits.js +5 -4
  22. package/dist/service/dynamodb/stream/sim-dynamodb-stream-shard-iterator.js +1 -3
  23. package/dist/service/dynamodb/time-to-live/sim-dynamodb-time-to-live.js +4 -3
  24. package/dist/service/iam/authorize/context/sim-iam-auth-z-allow-requirement.js +6 -6
  25. package/dist/service/kinesis/stream/sim-kinesis-shard-iterator.js +1 -3
  26. package/dist/service/lambda/destination/sim-lambda-destination-record.js +7 -7
  27. package/dist/service/lambda/function/policy/sim-lambda-permission.js +15 -14
  28. package/dist/service/lambda/metric/sim-lambda-function-metrics.js +11 -9
  29. package/dist/service/logs/metric/sim-logs-metric-filter.js +1 -8
  30. package/dist/service/route53/dns/answer/sim-route53-dns-zone-finder.js +5 -4
  31. package/dist/service/route53/resolve/sim-r53-zone-record-finder.js +8 -6
  32. package/dist/service/s3/bucket/notification/sim-s3-notification-filter.js +4 -7
  33. package/dist/service/s3/bucket/sim-s3-bucket.d.ts +8 -0
  34. package/dist/service/s3/bucket/sim-s3-bucket.js +11 -0
  35. package/dist/service/s3/bucket/sim-s3-key-writes.d.ts +16 -0
  36. package/dist/service/s3/bucket/sim-s3-key-writes.js +43 -0
  37. package/dist/service/s3/bucket/versioning/sim-s3-bucket-versioning.js +1 -3
  38. package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.command.d.ts +2 -1
  39. package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.handler.js +5 -1
  40. package/dist/service/s3/command/copy-object/copy-object-precondition.d.ts +23 -0
  41. package/dist/service/s3/command/copy-object/copy-object-precondition.js +34 -0
  42. package/dist/service/s3/command/copy-object/copy-object.command.d.ts +3 -1
  43. package/dist/service/s3/command/copy-object/copy-object.handler.d.ts +4 -0
  44. package/dist/service/s3/command/copy-object/copy-object.handler.js +11 -1
  45. package/dist/service/s3/command/object/sim-s3-write-precondition.d.ts +34 -0
  46. package/dist/service/s3/command/object/sim-s3-write-precondition.js +47 -0
  47. package/dist/service/s3/command/put-object/put-object.command.d.ts +2 -1
  48. package/dist/service/s3/command/put-object/put-object.handler.js +5 -1
  49. package/dist/service/s3/error/sim-s3.error.d.ts +10 -0
  50. package/dist/service/s3/error/sim-s3.error.js +12 -0
  51. package/dist/service/s3/object/s3-object-listing.js +4 -3
  52. package/dist/service/s3/serve/api/sim-s3-api-input.d.ts +4 -0
  53. package/dist/service/s3/serve/api/sim-s3-api-input.js +10 -0
  54. package/dist/service/s3/serve/api/sim-s3-api-multipart-input.js +2 -0
  55. package/dist/service/s3/serve/api/sim-s3-api-object-input.d.ts +6 -0
  56. package/dist/service/s3/serve/api/sim-s3-api-object-input.js +34 -2
  57. package/dist/service/s3/storage/filesystem/s3-filesystem-object-keys.js +9 -8
  58. package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-template.test-support.js +15 -23
  59. package/dist/service/sqs/message/sim-sqs-message.js +3 -3
  60. package/dist/service/stepfunctions/machine/sim-state-machine.js +4 -3
  61. package/dist/util/schedule/cron/sim-cron-field-bounds.js +1 -1
  62. package/docs/services/s3/README.md +68 -3
  63. package/package.json +2 -2
@@ -19,11 +19,12 @@ export class SimRoute53DnsZoneFinder {
19
19
  let bestZoneNameLength = -1;
20
20
  for (const hostedZone of this.hostedZones.values()) {
21
21
  const zoneName = simRoute53ZoneName(hostedZone);
22
- if (zoneContainsName(zoneName, name) &&
23
- zoneName.length > bestZoneNameLength) {
24
- bestZone = hostedZone;
25
- bestZoneNameLength = zoneName.length;
22
+ if (!zoneContainsName(zoneName, name) ||
23
+ zoneName.length <= bestZoneNameLength) {
24
+ continue;
26
25
  }
26
+ bestZone = hostedZone;
27
+ bestZoneNameLength = zoneName.length;
27
28
  }
28
29
  return bestZone;
29
30
  }
@@ -20,10 +20,11 @@ export class SimRoute53HostedZoneRecordFinder {
20
20
  continue;
21
21
  }
22
22
  const record = hostedZone.records.get(name, type);
23
- if (record !== undefined && zoneName.length > bestZoneNameLength) {
24
- bestRecord = record;
25
- bestZoneNameLength = zoneName.length;
23
+ if (record === undefined || zoneName.length <= bestZoneNameLength) {
24
+ continue;
26
25
  }
26
+ bestRecord = record;
27
+ bestZoneNameLength = zoneName.length;
27
28
  }
28
29
  return bestRecord;
29
30
  }
@@ -43,10 +44,11 @@ export class SimRoute53HostedZoneRecordFinder {
43
44
  if (!this.zoneContainsName(zoneName, name)) {
44
45
  continue;
45
46
  }
46
- if (zoneName.length > bestZoneNameLength) {
47
- bestHostedZone = hostedZone;
48
- bestZoneNameLength = zoneName.length;
47
+ if (zoneName.length <= bestZoneNameLength) {
48
+ continue;
49
49
  }
50
+ bestHostedZone = hostedZone;
51
+ bestZoneNameLength = zoneName.length;
50
52
  }
51
53
  return bestHostedZone;
52
54
  }
@@ -55,13 +55,10 @@ export class SimS3NotificationFilter {
55
55
  * filters nothing.
56
56
  */
57
57
  toOutput() {
58
- const rules = [];
59
- if (this.prefix !== "") {
60
- rules.push({ Name: "prefix", Value: this.prefix });
61
- }
62
- if (this.suffix !== "") {
63
- rules.push({ Name: "suffix", Value: this.suffix });
64
- }
58
+ const rules = [
59
+ ...(this.prefix === "" ? [] : [{ Name: "prefix", Value: this.prefix }]),
60
+ ...(this.suffix === "" ? [] : [{ Name: "suffix", Value: this.suffix }]),
61
+ ];
65
62
  if (rules.length === 0) {
66
63
  return undefined;
67
64
  }
@@ -23,9 +23,17 @@ export declare class SimS3Bucket extends SimS3BucketConfiguration {
23
23
  private readonly accountRegionScope;
24
24
  private readonly systemMetadata;
25
25
  private readonly objects;
26
+ private readonly keyWrites;
26
27
  constructor(properties: SimS3BucketProperties);
27
28
  /** Put an Object into storage, answering the version it was given, if any. */
28
29
  putObject(object: SimS3Object): Promise<SimS3ObjectVersion | undefined>;
30
+ /**
31
+ * Run a write to a key once every earlier write to it has finished.
32
+ *
33
+ * A write that checks the key before replacing it does both inside this,
34
+ * so no other write to the key lands between the check and the put.
35
+ */
36
+ writeKey<T>(key: string, write: () => Promise<T>): Promise<T>;
29
37
  /** Get a simulated S3 Object from storage. */
30
38
  getObject(key: string): Promise<SimS3Object | undefined>;
31
39
  /** List simulated S3 Objects from storage. */
@@ -1,6 +1,7 @@
1
1
  import { SimRealClock } from "../../../util/clock/sim-clock.js";
2
2
  import { MemoryS3BucketStorage } from "../storage/s3-memory-storage.js";
3
3
  import { SimS3BucketObjects } from "./sim-s3-bucket-objects.js";
4
+ import { SimS3KeyWrites } from "./sim-s3-key-writes.js";
4
5
  import { SimS3LifecycleConfiguration } from "./lifecycle/sim-s3-lifecycle-configuration.js";
5
6
  import { simS3BucketWebsiteUrl } from "./website/sim-s3-bucket-website-url.js";
6
7
  import { validateS3BucketName } from "./validate/validate-s3-bucket-name.js";
@@ -16,6 +17,7 @@ export class SimS3Bucket extends SimS3BucketConfiguration {
16
17
  accountRegionScope;
17
18
  systemMetadata = new SimS3BucketSystemMetadata();
18
19
  objects;
20
+ keyWrites = new SimS3KeyWrites();
19
21
  constructor(properties) {
20
22
  super(properties);
21
23
  const { bucketName, accountRegionScope = simAwsAccountRegionScopeFactory.make(), storage = new MemoryS3BucketStorage(), lifecycle = SimS3LifecycleConfiguration.empty(), clock = new SimRealClock(), creationDate = clock.now(), } = properties;
@@ -29,6 +31,15 @@ export class SimS3Bucket extends SimS3BucketConfiguration {
29
31
  async putObject(object) {
30
32
  return await this.objects.put(object);
31
33
  }
34
+ /**
35
+ * Run a write to a key once every earlier write to it has finished.
36
+ *
37
+ * A write that checks the key before replacing it does both inside this,
38
+ * so no other write to the key lands between the check and the put.
39
+ */
40
+ async writeKey(key, write) {
41
+ return await this.keyWrites.exclusive(key, write);
42
+ }
32
43
  /** Get a simulated S3 Object from storage. */
33
44
  async getObject(key) {
34
45
  return await this.objects.get(key);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The writes in progress against each key of a Bucket, run one at a time.
3
+ *
4
+ * A conditional write reads the key and then writes it, with awaits between
5
+ * the two. Real S3 lets the first of several conditional writes to one key
6
+ * finish and refuses the rest. Running each key's writes in turn gives the
7
+ * same answer, because a write's condition is read after the write before it
8
+ * has landed. Writes to different keys still run side by side.
9
+ */
10
+ export declare class SimS3KeyWrites {
11
+ private readonly pending;
12
+ /**
13
+ * Run a write once every earlier write to the same key has settled.
14
+ */
15
+ exclusive<T>(key: string, write: () => Promise<T>): Promise<T>;
16
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The writes in progress against each key of a Bucket, run one at a time.
3
+ *
4
+ * A conditional write reads the key and then writes it, with awaits between
5
+ * the two. Real S3 lets the first of several conditional writes to one key
6
+ * finish and refuses the rest. Running each key's writes in turn gives the
7
+ * same answer, because a write's condition is read after the write before it
8
+ * has landed. Writes to different keys still run side by side.
9
+ */
10
+ export class SimS3KeyWrites {
11
+ pending = new Map();
12
+ /**
13
+ * Run a write once every earlier write to the same key has settled.
14
+ */
15
+ async exclusive(key, write) {
16
+ const running = afterEarlier(this.pending.get(key), write);
17
+ const settled = settle(running);
18
+ this.pending.set(key, settled);
19
+ try {
20
+ return await running;
21
+ }
22
+ finally {
23
+ if (this.pending.get(key) === settled) {
24
+ this.pending.delete(key);
25
+ }
26
+ }
27
+ }
28
+ }
29
+ async function afterEarlier(earlier, write) {
30
+ await earlier;
31
+ return await write();
32
+ }
33
+ /**
34
+ * Wait for a write without taking on its failure, which its own caller sees.
35
+ */
36
+ async function settle(running) {
37
+ try {
38
+ await running;
39
+ }
40
+ catch {
41
+ // The next write to the key runs whether this one failed or not.
42
+ }
43
+ }
@@ -34,9 +34,7 @@ export class SimS3BucketVersioning {
34
34
  * The state a PutBucketVersioning request asks for.
35
35
  */
36
36
  static fromConfiguration(configuration) {
37
- return configuration.Status === "Enabled"
38
- ? new SimS3BucketVersioning("Enabled")
39
- : new SimS3BucketVersioning("Suspended");
37
+ return new SimS3BucketVersioning(configuration.Status === "Enabled" ? "Enabled" : "Suspended");
40
38
  }
41
39
  /**
42
40
  * What GetBucketVersioning answers with.
@@ -1,4 +1,5 @@
1
1
  import type { SimResponseMetadata } from "../../../aws/metadata/response-metadata.type.js";
2
+ import type { SimS3WritePrecondition } from "../object/sim-s3-write-precondition.js";
2
3
  /**
3
4
  * Minimal structural sim S3 CompleteMultipartUpload command.
4
5
  */
@@ -18,7 +19,7 @@ export interface SimCompletedUploadPart {
18
19
  * The parts are named by the request rather than assumed by S3, so a client can
19
20
  * finish an upload it sent a part of twice.
20
21
  */
21
- export interface SimCompleteMultipartUploadCommandInput {
22
+ export interface SimCompleteMultipartUploadCommandInput extends SimS3WritePrecondition {
22
23
  readonly Bucket?: string | undefined;
23
24
  readonly Key?: string | undefined;
24
25
  readonly UploadId?: string | undefined;
@@ -3,6 +3,7 @@ import { simS3BucketUrl } from "../../bucket/sim-s3-endpoint-url.js";
3
3
  import { simS3QuotedETag } from "../../object/s3-object-etag.js";
4
4
  import { simS3CompletedUploadObject, } from "../../upload/sim-s3-completed-upload.js";
5
5
  import { SimS3MultipartAccess, } from "../multipart/sim-s3-multipart-access.js";
6
+ import { simS3RequireWritePrecondition } from "../object/sim-s3-write-precondition.js";
6
7
  /**
7
8
  * Simulated S3 CompleteMultipartUploadCommand handler.
8
9
  *
@@ -34,7 +35,10 @@ export class CompleteMultipartUploadCommandHandler {
34
35
  parts: namedParts(command.input.MultipartUpload?.Parts ?? []),
35
36
  completedAt: this.access.now(),
36
37
  });
37
- const version = await bucket.putObject(object);
38
+ const version = await bucket.writeKey(Key, async () => {
39
+ await simS3RequireWritePrecondition(bucket, Key, command.input);
40
+ return await bucket.putObject(object);
41
+ });
38
42
  bucket.getMultipartUploads().discard(UploadId);
39
43
  this.notifications.objectCreated({
40
44
  bucket,
@@ -0,0 +1,23 @@
1
+ import type { SimS3Object } from "../../object/s3-object.js";
2
+ /**
3
+ * The conditions a copy can place on its source Object, as the
4
+ * `x-amz-copy-source-if-*` headers.
5
+ */
6
+ export interface SimS3CopySourcePrecondition {
7
+ readonly CopySourceIfMatch?: string | undefined;
8
+ readonly CopySourceIfNoneMatch?: string | undefined;
9
+ readonly CopySourceIfModifiedSince?: Date | undefined;
10
+ readonly CopySourceIfUnmodifiedSince?: Date | undefined;
11
+ }
12
+ /**
13
+ * Refuse a copy whose source Object fails one of its conditions.
14
+ *
15
+ * Real S3 follows RFC 7232 here. An ETag condition takes precedence over the
16
+ * date condition paired with it, so a source that matches `if-match` is copied
17
+ * however recently it changed, and a source that fails `if-none-match` is
18
+ * refused however long ago it changed. HTTP dates carry whole seconds, and the
19
+ * Object's last-modified time is compared at that precision.
20
+ *
21
+ * https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
22
+ */
23
+ export declare function simS3RequireCopySourcePrecondition(source: SimS3Object, condition: SimS3CopySourcePrecondition): void;
@@ -0,0 +1,34 @@
1
+ import { SimS3PreconditionFailed } from "../../error/sim-s3.error.js";
2
+ import { simS3ETagMatches, simS3PreconditionMessage, } from "../object/sim-s3-write-precondition.js";
3
+ /**
4
+ * Refuse a copy whose source Object fails one of its conditions.
5
+ *
6
+ * Real S3 follows RFC 7232 here. An ETag condition takes precedence over the
7
+ * date condition paired with it, so a source that matches `if-match` is copied
8
+ * however recently it changed, and a source that fails `if-none-match` is
9
+ * refused however long ago it changed. HTTP dates carry whole seconds, and the
10
+ * Object's last-modified time is compared at that precision.
11
+ *
12
+ * https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
13
+ */
14
+ export function simS3RequireCopySourcePrecondition(source, condition) {
15
+ if (!sourceConditionsHold(source, condition)) {
16
+ throw new SimS3PreconditionFailed(simS3PreconditionMessage);
17
+ }
18
+ }
19
+ function sourceConditionsHold(source, condition) {
20
+ const modified = wholeSeconds(source.lastModified);
21
+ const { CopySourceIfMatch: ifMatch, CopySourceIfNoneMatch: ifNoneMatch, CopySourceIfModifiedSince: ifModifiedSince, CopySourceIfUnmodifiedSince: ifUnmodifiedSince, } = condition;
22
+ const matchHolds = ifMatch === undefined
23
+ ? ifUnmodifiedSince === undefined ||
24
+ modified <= wholeSeconds(ifUnmodifiedSince)
25
+ : simS3ETagMatches(ifMatch, source.etag);
26
+ const noneMatchHolds = ifNoneMatch === undefined
27
+ ? ifModifiedSince === undefined ||
28
+ modified > wholeSeconds(ifModifiedSince)
29
+ : !simS3ETagMatches(ifNoneMatch, source.etag);
30
+ return matchHolds && noneMatchHolds;
31
+ }
32
+ function wholeSeconds(date) {
33
+ return Math.floor(date.getTime() / 1000);
34
+ }
@@ -2,6 +2,8 @@ import type { SimResponseMetadata } from "../../../aws/metadata/response-metadat
2
2
  import type { SimS3ObjectWriteMetadata } from "../../object/s3-write-metadata.js";
3
3
  import type { SimS3ObjectWriteStorage } from "../../object/s3-write-storage.js";
4
4
  import type { SimS3ObjectWriteTagging } from "../../object/s3-write-tagging.js";
5
+ import type { SimS3WritePrecondition } from "../object/sim-s3-write-precondition.js";
6
+ import type { SimS3CopySourcePrecondition } from "./copy-object-precondition.js";
5
7
  /**
6
8
  * Minimal structural sim S3 CopyObject command.
7
9
  */
@@ -15,7 +17,7 @@ export interface SimCopyObjectCommand {
15
17
  * apply to the destination Object under `MetadataDirective: REPLACE`. Under
16
18
  * the default `COPY` they are ignored, as real S3 ignores them.
17
19
  */
18
- export interface SimCopyObjectCommandInput extends SimS3ObjectWriteMetadata, SimS3ObjectWriteStorage, SimS3ObjectWriteTagging {
20
+ export interface SimCopyObjectCommandInput extends SimS3ObjectWriteMetadata, SimS3ObjectWriteStorage, SimS3ObjectWriteTagging, SimS3WritePrecondition, SimS3CopySourcePrecondition {
19
21
  readonly Bucket?: string | undefined;
20
22
  readonly Key?: string | undefined;
21
23
  /** The source Object, as `sourceBucket/sourceKey`, URL-encoded. */
@@ -36,6 +36,10 @@ export declare class CopyObjectCommandHandler implements CommandHandler<SimCopyO
36
36
  * decision. That keeps a malformed request and a missing Bucket answering
37
37
  * what real S3 answers. The source Object is read after authorization, which
38
38
  * leaves a refused caller knowing nothing about which keys exist.
39
+ *
40
+ * The conditions on the source are checked once it has been read, and the
41
+ * conditions on the destination after that. Either one failing refuses the
42
+ * copy before anything is written.
39
43
  */
40
44
  handle(command: SimCopyObjectCommand, options?: SimS3RequestOptions): Promise<SimCopyObjectCommandOutput>;
41
45
  }
@@ -4,8 +4,10 @@ import { SimIamAllowAllAuth, } from "../../../iam/authorize/sim-iam-inter-servic
4
4
  import { simS3QuotedETag } from "../../object/s3-object-etag.js";
5
5
  import { GetObjectAuthorizer } from "../get-object/get-object-authorizer.js";
6
6
  import { PutObjectAuthorizer } from "../put-object/put-object-authorizer.js";
7
+ import { simS3RequireWritePrecondition } from "../object/sim-s3-write-precondition.js";
7
8
  import { requireSimS3Bucket } from "../require-sim-s3-bucket.js";
8
9
  import { CopyObjectBuilder } from "./copy-object-builder.js";
10
+ import { simS3RequireCopySourcePrecondition } from "./copy-object-precondition.js";
9
11
  import { simS3CopySource, simS3CopySourceObject, simS3RefuseUnchangedSelfCopy, } from "./copy-object-source.js";
10
12
  /**
11
13
  * Simulated S3 CopyObjectCommand handler.
@@ -40,6 +42,10 @@ export class CopyObjectCommandHandler {
40
42
  * decision. That keeps a malformed request and a missing Bucket answering
41
43
  * what real S3 answers. The source Object is read after authorization, which
42
44
  * leaves a refused caller knowing nothing about which keys exist.
45
+ *
46
+ * The conditions on the source are checked once it has been read, and the
47
+ * conditions on the destination after that. Either one failing refuses the
48
+ * copy before anything is written.
43
49
  */
44
50
  async handle(command, options) {
45
51
  assertDefined(command.input.Bucket, "CopyObjectCommand.input.Bucket");
@@ -56,8 +62,12 @@ export class CopyObjectCommandHandler {
56
62
  this.sourceAuthorizer.authorize(source, from.key, options);
57
63
  const caller = this.destinationAuthorizer.authorize(destination, key, options);
58
64
  const stored = await simS3CopySourceObject(source, from.key);
65
+ simS3RequireCopySourcePrecondition(stored, command.input);
59
66
  const object = this.objectBuilder.build(command.input, key, stored, destination.getEncryption().algorithm);
60
- const version = await destination.putObject(object);
67
+ const version = await destination.writeKey(key, async () => {
68
+ await simS3RequireWritePrecondition(destination, key, command.input);
69
+ return await destination.putObject(object);
70
+ });
61
71
  this.notifications.objectCreated({
62
72
  bucket: destination,
63
73
  object,
@@ -0,0 +1,34 @@
1
+ import type { SimS3Bucket } from "../../bucket/sim-s3-bucket.js";
2
+ /**
3
+ * The conditions a write can place on the Object already under its key.
4
+ *
5
+ * PutObject, CopyObject and CompleteMultipartUpload all take these, as the
6
+ * `If-None-Match` and `If-Match` headers.
7
+ *
8
+ * https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html
9
+ */
10
+ export interface SimS3WritePrecondition {
11
+ readonly IfMatch?: string | undefined;
12
+ readonly IfNoneMatch?: string | undefined;
13
+ }
14
+ /**
15
+ * The message real S3 gives with `PreconditionFailed`.
16
+ */
17
+ export declare const simS3PreconditionMessage = "At least one of the pre-conditions you specified did not hold";
18
+ /**
19
+ * Refuse a write whose condition does not hold against the key's current
20
+ * Object.
21
+ *
22
+ * `If-None-Match: *` fails when the key holds an Object. `If-Match` fails when
23
+ * the Object's ETag differs, and answers `NoSuchKey` when the key holds
24
+ * nothing. A delete marker counts as nothing, because a read of the key finds
25
+ * nothing behind it. Real S3 accepts only `*` for `If-None-Match` on a write.
26
+ */
27
+ export declare function simS3RequireWritePrecondition(bucket: SimS3Bucket, key: string, condition: SimS3WritePrecondition): Promise<void>;
28
+ /**
29
+ * Whether an entity tag a client sent names an Object's ETag.
30
+ *
31
+ * `*` names any Object. A client sends back the ETag it was given, quotes and
32
+ * all, so both forms are accepted.
33
+ */
34
+ export declare function simS3ETagMatches(sent: string, etag: string): boolean;
@@ -0,0 +1,47 @@
1
+ import { SimS3NoSuchKey, SimS3NotImplemented, SimS3PreconditionFailed, } from "../../error/sim-s3.error.js";
2
+ import { simS3UnquotedETag } from "../../object/s3-object-etag.js";
3
+ /**
4
+ * The message real S3 gives with `PreconditionFailed`.
5
+ */
6
+ export const simS3PreconditionMessage = "At least one of the pre-conditions you specified did not hold";
7
+ /**
8
+ * Refuse a write whose condition does not hold against the key's current
9
+ * Object.
10
+ *
11
+ * `If-None-Match: *` fails when the key holds an Object. `If-Match` fails when
12
+ * the Object's ETag differs, and answers `NoSuchKey` when the key holds
13
+ * nothing. A delete marker counts as nothing, because a read of the key finds
14
+ * nothing behind it. Real S3 accepts only `*` for `If-None-Match` on a write.
15
+ */
16
+ export async function simS3RequireWritePrecondition(bucket, key, condition) {
17
+ const { IfMatch, IfNoneMatch } = condition;
18
+ if (IfMatch === undefined && IfNoneMatch === undefined) {
19
+ return;
20
+ }
21
+ if (IfNoneMatch !== undefined && IfNoneMatch !== "*") {
22
+ throw new SimS3NotImplemented(`A header you provided implies functionality that is not implemented. ` +
23
+ `If-None-Match on a write takes only *.`);
24
+ }
25
+ const current = await bucket.getObject(key);
26
+ if (IfNoneMatch !== undefined && current !== undefined) {
27
+ throw new SimS3PreconditionFailed(simS3PreconditionMessage);
28
+ }
29
+ if (IfMatch === undefined) {
30
+ return;
31
+ }
32
+ if (current === undefined) {
33
+ throw new SimS3NoSuchKey(`No S3 Object named ${key}`);
34
+ }
35
+ if (!simS3ETagMatches(IfMatch, current.etag)) {
36
+ throw new SimS3PreconditionFailed(simS3PreconditionMessage);
37
+ }
38
+ }
39
+ /**
40
+ * Whether an entity tag a client sent names an Object's ETag.
41
+ *
42
+ * `*` names any Object. A client sends back the ETag it was given, quotes and
43
+ * all, so both forms are accepted.
44
+ */
45
+ export function simS3ETagMatches(sent, etag) {
46
+ return sent.trim() === "*" || simS3UnquotedETag(sent).trim() === etag;
47
+ }
@@ -3,6 +3,7 @@ import type { SimS3WriteBody } from "../../object/s3-write-body.js";
3
3
  import type { SimS3ObjectWriteMetadata } from "../../object/s3-write-metadata.js";
4
4
  import type { SimS3ObjectWriteStorage } from "../../object/s3-write-storage.js";
5
5
  import type { SimS3ObjectWriteTagging } from "../../object/s3-write-tagging.js";
6
+ import type { SimS3WritePrecondition } from "../object/sim-s3-write-precondition.js";
6
7
  /**
7
8
  * Minimal structural sim S3 PutObject command.
8
9
  */
@@ -16,7 +17,7 @@ export interface SimPutObjectCommand {
16
17
  * Object carries, which is why they are declared once in
17
18
  * `SimS3ObjectWriteMetadata` and shared with `CreateMultipartUpload`.
18
19
  */
19
- export interface SimPutObjectCommandInput extends SimS3ObjectWriteMetadata, SimS3ObjectWriteStorage, SimS3ObjectWriteTagging {
20
+ export interface SimPutObjectCommandInput extends SimS3ObjectWriteMetadata, SimS3ObjectWriteStorage, SimS3ObjectWriteTagging, SimS3WritePrecondition {
20
21
  readonly Bucket?: string | undefined;
21
22
  readonly Key?: string | undefined;
22
23
  readonly Body?: SimPutObjectBody;
@@ -5,6 +5,7 @@ import { requireSimS3Bucket } from "../require-sim-s3-bucket.js";
5
5
  import { PutObjectAuthorizer } from "./put-object-authorizer.js";
6
6
  import { PutObjectBuilder } from "./put-object-builder.js";
7
7
  import { simS3QuotedETag } from "../../object/s3-object-etag.js";
8
+ import { simS3RequireWritePrecondition } from "../object/sim-s3-write-precondition.js";
8
9
  /**
9
10
  * Simulated S3 PutObjectCommand handler.
10
11
  *
@@ -45,7 +46,10 @@ export class PutObjectCommandHandler {
45
46
  await this.background.sequence();
46
47
  const caller = this.authorizer.authorize(bucket, Key, options);
47
48
  const object = this.objectBuilder.build(command, bucket.getEncryption().algorithm);
48
- const version = await bucket.putObject(object);
49
+ const version = await bucket.writeKey(Key, async () => {
50
+ await simS3RequireWritePrecondition(bucket, Key, command.input);
51
+ return await bucket.putObject(object);
52
+ });
49
53
  const versionId = version?.versionId;
50
54
  // Every write path funnels through here, so this one call covers the SDK,
51
55
  // an intercepted SDK client and the REST endpoint alike.
@@ -199,3 +199,13 @@ export declare class SimS3InvalidTag extends SimS3Error {
199
199
  readonly name = "InvalidTag";
200
200
  constructor(message: string);
201
201
  }
202
+ /**
203
+ * Simulated S3 PreconditionFailed error.
204
+ *
205
+ * Real S3 answers this when a conditional request's condition does not hold.
206
+ * A write refused this way leaves the Object under its key as it was.
207
+ */
208
+ export declare class SimS3PreconditionFailed extends SimS3Error {
209
+ readonly name = "PreconditionFailed";
210
+ constructor(message: string);
211
+ }
@@ -234,3 +234,15 @@ export class SimS3InvalidTag extends SimS3Error {
234
234
  super(message, { httpStatusCode: 400 });
235
235
  }
236
236
  }
237
+ /**
238
+ * Simulated S3 PreconditionFailed error.
239
+ *
240
+ * Real S3 answers this when a conditional request's condition does not hold.
241
+ * A write refused this way leaves the Object under its key as it was.
242
+ */
243
+ export class SimS3PreconditionFailed extends SimS3Error {
244
+ name = "PreconditionFailed";
245
+ constructor(message) {
246
+ super(message, { httpStatusCode: 412 });
247
+ }
248
+ }
@@ -87,10 +87,11 @@ function listingEntries(request) {
87
87
  entries.push({ name: object.key, object });
88
88
  continue;
89
89
  }
90
- if (commonPrefix !== rolledUp) {
91
- rolledUp = commonPrefix;
92
- entries.push({ name: commonPrefix });
90
+ if (commonPrefix === rolledUp) {
91
+ continue;
93
92
  }
93
+ rolledUp = commonPrefix;
94
+ entries.push({ name: commonPrefix });
94
95
  }
95
96
  return entries;
96
97
  }
@@ -31,6 +31,10 @@ export declare function listObjectsInput(request: SimS3ApiRequest): object;
31
31
  * an absent member and an empty one differently.
32
32
  */
33
33
  export declare function optional(name: string, value: string | null): Record<string, string>;
34
+ /**
35
+ * The date form of the same, dropping a value that is not a date.
36
+ */
37
+ export declare function optionalDate(name: string, value: string | null): Record<string, Date>;
34
38
  /**
35
39
  * The numeric form of the same, dropping a value that is not a number.
36
40
  */
@@ -61,6 +61,16 @@ export function listObjectsInput(request) {
61
61
  export function optional(name, value) {
62
62
  return value === null ? {} : { [name]: value };
63
63
  }
64
+ /**
65
+ * The date form of the same, dropping a value that is not a date.
66
+ */
67
+ export function optionalDate(name, value) {
68
+ if (value === null) {
69
+ return {};
70
+ }
71
+ const parsed = new Date(value);
72
+ return Number.isNaN(parsed.getTime()) ? {} : { [name]: parsed };
73
+ }
64
74
  /**
65
75
  * The numeric form of the same, dropping a value that is not a number.
66
76
  */
@@ -1,4 +1,5 @@
1
1
  import { optional, optionalNumber } from "./sim-s3-api-input.js";
2
+ import { writePreconditionInput } from "./sim-s3-api-object-input.js";
2
3
  import { readSimS3CompletedUpload } from "./sim-s3-api-xml-read.js";
3
4
  /**
4
5
  * Reading the multipart upload operations' inputs out of a request.
@@ -33,6 +34,7 @@ export function uploadPartInput(request) {
33
34
  export function completeMultipartUploadInput(request) {
34
35
  return {
35
36
  ...uploadInput(request),
37
+ ...writePreconditionInput(request),
36
38
  MultipartUpload: readSimS3CompletedUpload(request.body),
37
39
  };
38
40
  }
@@ -35,6 +35,12 @@ export declare function getObjectInput(request: SimS3ApiRequest): object;
35
35
  * them.
36
36
  */
37
37
  export declare function putObjectInput(request: SimS3ApiRequest): object;
38
+ /**
39
+ * The conditions a write places on the Object already under its key, which
40
+ * PutObject, CopyObject and CompleteMultipartUpload all read from the same two
41
+ * headers.
42
+ */
43
+ export declare function writePreconditionInput(request: SimS3ApiRequest): object;
38
44
  /**
39
45
  * The input of a copy, which names its source in a header and carries no bytes
40
46
  * of its own.
@@ -1,5 +1,5 @@
1
1
  import { simS3WriteMetadataHeaders } from "../../object/s3-write-metadata.js";
2
- import { optional, utf8 } from "./sim-s3-api-input.js";
2
+ import { optional, optionalDate, utf8 } from "./sim-s3-api-input.js";
3
3
  import { readSimS3Tagging } from "./sim-s3-api-tagging-read.js";
4
4
  /**
5
5
  * Reading the Object operations' inputs out of a request.
@@ -54,7 +54,22 @@ export function getObjectInput(request) {
54
54
  * them.
55
55
  */
56
56
  export function putObjectInput(request) {
57
- return { ...createMultipartUploadInput(request), Body: request.body };
57
+ return {
58
+ ...createMultipartUploadInput(request),
59
+ ...writePreconditionInput(request),
60
+ Body: request.body,
61
+ };
62
+ }
63
+ /**
64
+ * The conditions a write places on the Object already under its key, which
65
+ * PutObject, CopyObject and CompleteMultipartUpload all read from the same two
66
+ * headers.
67
+ */
68
+ export function writePreconditionInput(request) {
69
+ return {
70
+ ...optional("IfMatch", request.headers.get("if-match")),
71
+ ...optional("IfNoneMatch", request.headers.get("if-none-match")),
72
+ };
58
73
  }
59
74
  /**
60
75
  * The input of a copy, which names its source in a header and carries no bytes
@@ -67,11 +82,28 @@ export function putObjectInput(request) {
67
82
  export function copyObjectInput(request) {
68
83
  return {
69
84
  ...createMultipartUploadInput(request),
85
+ ...writePreconditionInput(request),
86
+ ...copySourcePreconditionInput(request),
70
87
  ...optional("CopySource", request.headers.get(simS3CopySourceHeader)),
71
88
  ...optional("MetadataDirective", request.headers.get("x-amz-metadata-directive")),
72
89
  ...optional("TaggingDirective", request.headers.get("x-amz-tagging-directive")),
73
90
  };
74
91
  }
92
+ /**
93
+ * The conditions a copy places on its source Object.
94
+ *
95
+ * The two date headers are HTTP dates, which the SDK hands the operation as
96
+ * `Date` values.
97
+ */
98
+ function copySourcePreconditionInput(request) {
99
+ const header = (name) => request.headers.get(`x-amz-copy-source-if-${name}`);
100
+ return {
101
+ ...optional("CopySourceIfMatch", header("match")),
102
+ ...optional("CopySourceIfNoneMatch", header("none-match")),
103
+ ...optionalDate("CopySourceIfModifiedSince", header("modified-since")),
104
+ ...optionalDate("CopySourceIfUnmodifiedSince", header("unmodified-since")),
105
+ };
106
+ }
75
107
  /**
76
108
  * The input of a request that describes an Object without carrying its bytes,
77
109
  * which is what starting a multipart upload is.