@kensio/yulin 1.21.30 → 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.
- package/dist/serve/message/sim-message-log-console.js +3 -3
- package/dist/service/apigateway/api/deployment/sim-rest-api-deployment.js +3 -3
- package/dist/service/apigateway/api/method/sim-rest-api-method.js +12 -12
- package/dist/service/apigateway/api/resource/sim-rest-api-resource.js +11 -9
- package/dist/service/apigateway/api/sim-rest-api-view.js +3 -3
- package/dist/service/apigateway/api/stage/sim-rest-api-stage.js +8 -10
- package/dist/service/apigatewayv2/api/integration/sim-http-api-integration.js +3 -3
- package/dist/service/apigatewayv2/api/route/sim-http-api-route-view.js +6 -6
- package/dist/service/apigatewayv2/api/sim-http-api-view.js +3 -3
- package/dist/service/apigatewayv2/api/stage/settings/sim-http-api-stage-route-settings.js +8 -7
- package/dist/service/apigatewayv2/api/stage/sim-http-api-stage.js +9 -9
- package/dist/service/athena/engine/sim-athena-partitioned-objects.js +7 -7
- package/dist/service/athena/sim-athena-engine.fixture.js +3 -0
- package/dist/service/athena/table/sim-athena-partition-filters.js +6 -5
- package/dist/service/backup/command/sim-backup-authorizer.js +5 -4
- package/dist/service/bedrock/response/sim-bedrock-declared-content.js +5 -10
- package/dist/service/cloudformation/stack/update/sim-cfn-stack-resource-changes.js +19 -10
- package/dist/service/cloudfront/cfn/response-headers-policy/sim-cfn-cf-rh-policy-security-values.js +2 -6
- package/dist/service/cognito/user-pool/trigger/sim-cognito-pre-sign-up-response.js +4 -7
- package/dist/service/dynamodb/command/transact/sim-dynamodb-transact-write.js +34 -25
- package/dist/service/dynamodb/secondary-index/sim-dynamodb-index-limits.js +5 -4
- package/dist/service/dynamodb/stream/sim-dynamodb-stream-shard-iterator.js +1 -3
- package/dist/service/dynamodb/time-to-live/sim-dynamodb-time-to-live.js +4 -3
- package/dist/service/iam/authorize/context/sim-iam-auth-z-allow-requirement.js +6 -6
- package/dist/service/kinesis/stream/sim-kinesis-shard-iterator.js +1 -3
- package/dist/service/lambda/destination/sim-lambda-destination-record.js +7 -7
- package/dist/service/lambda/function/outbound/sim-lambda-aws-api-outbound.d.ts +7 -1
- package/dist/service/lambda/function/outbound/sim-lambda-aws-api-outbound.js +15 -7
- package/dist/service/lambda/function/outbound/sim-lambda-http2-session.d.ts +3 -0
- package/dist/service/lambda/function/outbound/sim-lambda-http2-session.js +4 -0
- package/dist/service/lambda/function/policy/sim-lambda-permission.js +15 -14
- package/dist/service/lambda/metric/sim-lambda-function-metrics.js +11 -9
- package/dist/service/logs/metric/sim-logs-metric-filter.js +1 -8
- package/dist/service/route53/dns/answer/sim-route53-dns-zone-finder.js +5 -4
- package/dist/service/route53/resolve/sim-r53-zone-record-finder.js +8 -6
- package/dist/service/s3/bucket/notification/sim-s3-notification-filter.js +4 -7
- package/dist/service/s3/bucket/sim-s3-bucket.d.ts +8 -0
- package/dist/service/s3/bucket/sim-s3-bucket.js +11 -0
- package/dist/service/s3/bucket/sim-s3-key-writes.d.ts +16 -0
- package/dist/service/s3/bucket/sim-s3-key-writes.js +43 -0
- package/dist/service/s3/bucket/versioning/sim-s3-bucket-versioning.js +1 -3
- package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.command.d.ts +2 -1
- package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.handler.js +5 -1
- package/dist/service/s3/command/copy-object/copy-object-precondition.d.ts +23 -0
- package/dist/service/s3/command/copy-object/copy-object-precondition.js +34 -0
- package/dist/service/s3/command/copy-object/copy-object.command.d.ts +3 -1
- package/dist/service/s3/command/copy-object/copy-object.handler.d.ts +4 -0
- package/dist/service/s3/command/copy-object/copy-object.handler.js +11 -1
- package/dist/service/s3/command/object/sim-s3-write-precondition.d.ts +34 -0
- package/dist/service/s3/command/object/sim-s3-write-precondition.js +47 -0
- package/dist/service/s3/command/put-object/put-object.command.d.ts +2 -1
- package/dist/service/s3/command/put-object/put-object.handler.js +5 -1
- package/dist/service/s3/error/sim-s3.error.d.ts +10 -0
- package/dist/service/s3/error/sim-s3.error.js +12 -0
- package/dist/service/s3/object/s3-object-listing.js +4 -3
- package/dist/service/s3/serve/api/sim-s3-api-input.d.ts +4 -0
- package/dist/service/s3/serve/api/sim-s3-api-input.js +10 -0
- package/dist/service/s3/serve/api/sim-s3-api-multipart-input.js +2 -0
- package/dist/service/s3/serve/api/sim-s3-api-object-input.d.ts +6 -0
- package/dist/service/s3/serve/api/sim-s3-api-object-input.js +34 -2
- package/dist/service/s3/storage/filesystem/s3-filesystem-object-keys.js +9 -8
- package/dist/service/secretsmanager/cfn/secret/sim-cfn-secrets-manager-secret-template.test-support.js +15 -23
- package/dist/service/sqs/message/sim-sqs-message.js +3 -3
- package/dist/service/stepfunctions/machine/sim-state-machine.js +4 -3
- package/dist/util/schedule/cron/sim-cron-field-bounds.js +1 -1
- package/docs/services/s3/README.md +68 -3
- package/package.json +2 -2
|
@@ -42,11 +42,15 @@ export class SimLambdaHttp2Session {
|
|
|
42
42
|
*
|
|
43
43
|
* A request the simulation cannot answer fails the client's stream with the
|
|
44
44
|
* error that explains it, which is what the SDK rejects the call with.
|
|
45
|
+
*
|
|
46
|
+
* The answer carries the `date` the simulation gave it, or none. The server
|
|
47
|
+
* would otherwise date it from the host clock.
|
|
45
48
|
*/
|
|
46
49
|
async respond(request, response, clientStream) {
|
|
47
50
|
try {
|
|
48
51
|
const answered = await this.outbound.fetch(await simLambdaHttp2Request(this.authority, this.outbound, request));
|
|
49
52
|
const body = Buffer.from(await answered.arrayBuffer());
|
|
53
|
+
response.sendDate = false;
|
|
50
54
|
response.writeHead(answered.status, simLambdaHttp2ResponseHeaders(answered.headers));
|
|
51
55
|
response.end(body);
|
|
52
56
|
}
|
|
@@ -82,20 +82,21 @@ export class SimLambdaPermission {
|
|
|
82
82
|
*/
|
|
83
83
|
condition() {
|
|
84
84
|
const stringEquals = this.stringEqualsConditions();
|
|
85
|
-
const condition = {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
const condition = {
|
|
86
|
+
...(Object.keys(stringEquals).length > 0 && {
|
|
87
|
+
StringEquals: stringEquals,
|
|
88
|
+
}),
|
|
89
|
+
...(this.sourceArn !== undefined && {
|
|
90
|
+
ArnLike: {
|
|
91
|
+
[simLambdaSourceArnConditionKey]: this.sourceArn,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
...(this.invokedViaFunctionUrl !== undefined && {
|
|
95
|
+
Bool: {
|
|
96
|
+
[simLambdaInvokedViaFunctionUrlConditionKey]: this.invokedViaFunctionUrl,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
99
100
|
return Object.keys(condition).length === 0 ? undefined : condition;
|
|
100
101
|
}
|
|
101
102
|
stringEqualsConditions() {
|
|
@@ -69,15 +69,17 @@ function invocationData(outcome) {
|
|
|
69
69
|
value: outcome.durationMilliseconds,
|
|
70
70
|
unit: "Milliseconds",
|
|
71
71
|
},
|
|
72
|
+
...(outcome.failed
|
|
73
|
+
? [
|
|
74
|
+
{
|
|
75
|
+
namespace: simLambdaMetricNamespace,
|
|
76
|
+
metricName: "Errors",
|
|
77
|
+
dimensions,
|
|
78
|
+
value: 1,
|
|
79
|
+
unit: "Count",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
72
83
|
];
|
|
73
|
-
if (outcome.failed) {
|
|
74
|
-
data.push({
|
|
75
|
-
namespace: simLambdaMetricNamespace,
|
|
76
|
-
metricName: "Errors",
|
|
77
|
-
dimensions,
|
|
78
|
-
value: 1,
|
|
79
|
-
unit: "Count",
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
84
|
return data;
|
|
83
85
|
}
|
|
@@ -84,12 +84,5 @@ export class SimLogsMetricFilter {
|
|
|
84
84
|
* A batch's events grouped into the aggregation periods they were taken in.
|
|
85
85
|
*/
|
|
86
86
|
function byPeriod(events) {
|
|
87
|
-
|
|
88
|
-
for (const event of events) {
|
|
89
|
-
const period = Math.floor(event.ingestionTime / periodMilliseconds) * periodMilliseconds;
|
|
90
|
-
const inPeriod = grouped.get(period) ?? [];
|
|
91
|
-
inPeriod.push(event);
|
|
92
|
-
grouped.set(period, inPeriod);
|
|
93
|
-
}
|
|
94
|
-
return grouped;
|
|
87
|
+
return Map.groupBy(events, (event) => Math.floor(event.ingestionTime / periodMilliseconds) * periodMilliseconds);
|
|
95
88
|
}
|
|
@@ -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
|
|
24
|
-
|
|
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
|
|
24
|
-
|
|
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
|
|
47
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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.
|
package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.command.d.ts
CHANGED
|
@@ -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;
|
package/dist/service/s3/command/complete-multipart-upload/complete-multipart-upload.handler.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
91
|
-
|
|
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
|
*/
|