Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit bda65cc

Browse files
committedMay 15, 2023
Add support for multiple, weak, wildcard etags in R2 gateway
cloudflare/workerd#563 added support to R2 bindings for these

File tree

3 files changed

+113
-31
lines changed

3 files changed

+113
-31
lines changed
 

‎packages/tre/src/plugins/r2/schemas.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,20 @@ export const R2RangeSchema = z.object({
9494
});
9595
export type R2Range = z.infer<typeof R2RangeSchema>;
9696

97+
export const R2EtagSchema = z.discriminatedUnion("type", [
98+
z.object({ type: z.literal("strong"), value: z.string() }),
99+
z.object({ type: z.literal("weak"), value: z.string() }),
100+
z.object({ type: z.literal("wildcard") }),
101+
]);
102+
export type R2Etag = z.infer<typeof R2EtagSchema>;
103+
export const R2EtagMatchSchema = R2EtagSchema.array().min(1).optional();
104+
97105
// For more information, refer to https://datatracker.ietf.org/doc/html/rfc7232
98106
export const R2ConditionalSchema = z.object({
99107
// Performs the operation if the object's ETag matches the given string
100-
etagMatches: z.ostring(), // "If-Match"
108+
etagMatches: R2EtagMatchSchema, // "If-Match"
101109
// Performs the operation if the object's ETag does NOT match the given string
102-
etagDoesNotMatch: z.ostring(), // "If-None-Match"
110+
etagDoesNotMatch: R2EtagMatchSchema, // "If-None-Match"
103111
// Performs the operation if the object was uploaded BEFORE the given date
104112
uploadedBefore: DateSchema.optional(), // "If-Unmodified-Since"
105113
// Performs the operation if the object was uploaded AFTER the given date

‎packages/tre/src/plugins/r2/validator.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
PreconditionFailed,
1313
} from "./errors";
1414
import { R2Object } from "./r2Object";
15-
import { R2Conditional, R2GetOptions } from "./schemas";
15+
import { R2Conditional, R2Etag, R2GetOptions } from "./schemas";
1616

1717
export const MAX_LIST_KEYS = 1_000;
1818
const MAX_KEY_SIZE = 1024;
@@ -27,6 +27,21 @@ function truncateToSeconds(ms: number) {
2727
return Math.floor(ms / 1000) * 1000;
2828
}
2929

30+
function includesEtag(
31+
conditions: R2Etag[],
32+
etag: string,
33+
comparison: "strong" | "weak"
34+
) {
35+
// Adapted from internal R2 gateway implementation.
36+
for (const condition of conditions) {
37+
if (condition.type === "wildcard") return true;
38+
if (condition.value === etag) {
39+
if (condition.type === "strong" || comparison === "weak") return true;
40+
}
41+
}
42+
return false;
43+
}
44+
3045
// Returns `true` iff the condition passed
3146
/** @internal */
3247
export function _testR2Conditional(
@@ -43,9 +58,12 @@ export function _testR2Conditional(
4358
}
4459

4560
const { etag, uploaded: lastModifiedRaw } = metadata;
46-
const ifMatch = cond.etagMatches === undefined || cond.etagMatches === etag;
61+
const ifMatch =
62+
cond.etagMatches === undefined ||
63+
includesEtag(cond.etagMatches, etag, "strong");
4764
const ifNoneMatch =
48-
cond.etagDoesNotMatch === undefined || cond.etagDoesNotMatch !== etag;
65+
cond.etagDoesNotMatch === undefined ||
66+
!includesEtag(cond.etagDoesNotMatch, etag, "weak");
4967

5068
const maybeTruncate = cond.secondsGranularity ? truncateToSeconds : identity;
5169
const lastModified = maybeTruncate(lastModifiedRaw);

‎packages/tre/test/plugins/r2/validator.spec.ts

+82-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { R2Conditional } from "@cloudflare/workers-types/experimental";
2-
import { R2Object, _testR2Conditional } from "@miniflare/tre";
1+
import { R2Conditional, R2Object, _testR2Conditional } from "@miniflare/tre";
32
import test from "ava";
43

54
test("testR2Conditional: matches various conditions", (t) => {
@@ -20,89 +19,146 @@ test("testR2Conditional: matches various conditions", (t) => {
2019
const usingMissing = (cond: R2Conditional) => _testR2Conditional(cond);
2120

2221
// Check single conditions
23-
t.true(using({ etagMatches: etag }));
24-
t.false(using({ etagMatches: badEtag }));
22+
t.true(using({ etagMatches: [{ type: "strong", value: etag }] }));
23+
t.false(using({ etagMatches: [{ type: "strong", value: badEtag }] }));
2524

26-
t.true(using({ etagDoesNotMatch: badEtag }));
27-
t.false(using({ etagDoesNotMatch: etag }));
25+
t.true(using({ etagDoesNotMatch: [{ type: "strong", value: badEtag }] }));
26+
t.false(using({ etagDoesNotMatch: [{ type: "strong", value: etag }] }));
2827

2928
t.false(using({ uploadedBefore: pastDate }));
3029
t.true(using({ uploadedBefore: futureDate }));
3130

3231
t.true(using({ uploadedAfter: pastDate }));
3332
t.false(using({ uploadedBefore: pastDate }));
3433

34+
// Check with weaker etags
35+
t.false(using({ etagMatches: [{ type: "weak", value: etag }] }));
36+
t.false(using({ etagDoesNotMatch: [{ type: "weak", value: etag }] }));
37+
t.true(using({ etagDoesNotMatch: [{ type: "weak", value: badEtag }] }));
38+
t.true(using({ etagMatches: [{ type: "wildcard" }] }));
39+
t.false(using({ etagDoesNotMatch: [{ type: "wildcard" }] }));
40+
3541
// Check multiple conditions that evaluate to false
36-
t.false(using({ etagMatches: etag, etagDoesNotMatch: etag }));
37-
t.false(using({ etagMatches: etag, uploadedAfter: futureDate }));
42+
t.false(
43+
using({
44+
etagMatches: [{ type: "strong", value: etag }],
45+
etagDoesNotMatch: [{ type: "strong", value: etag }],
46+
})
47+
);
48+
t.false(
49+
using({
50+
etagMatches: [{ type: "strong", value: etag }],
51+
uploadedAfter: futureDate,
52+
})
53+
);
3854
t.false(
3955
using({
4056
// `etagMatches` pass makes `uploadedBefore` pass, but `uploadedAfter` fails
41-
etagMatches: etag,
57+
etagMatches: [{ type: "strong", value: etag }],
4258
uploadedAfter: futureDate,
4359
uploadedBefore: pastDate,
4460
})
4561
);
46-
t.false(using({ etagDoesNotMatch: badEtag, uploadedBefore: pastDate }));
62+
t.false(
63+
using({
64+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
65+
uploadedBefore: pastDate,
66+
})
67+
);
4768
t.false(
4869
using({
4970
// `etagDoesNotMatch` pass makes `uploadedAfter` pass, but `uploadedBefore` fails
50-
etagDoesNotMatch: badEtag,
71+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
5172
uploadedAfter: futureDate,
5273
uploadedBefore: pastDate,
5374
})
5475
);
5576
t.false(
5677
using({
57-
etagMatches: badEtag,
58-
etagDoesNotMatch: badEtag,
78+
etagMatches: [{ type: "strong", value: badEtag }],
79+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
5980
uploadedAfter: pastDate,
6081
uploadedBefore: futureDate,
6182
})
6283
);
6384

6485
// Check multiple conditions that evaluate to true
65-
t.true(using({ etagMatches: etag, etagDoesNotMatch: badEtag }));
86+
t.true(
87+
using({
88+
etagMatches: [{ type: "strong", value: etag }],
89+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
90+
})
91+
);
6692
// `etagMatches` pass makes `uploadedBefore` pass
67-
t.true(using({ etagMatches: etag, uploadedBefore: pastDate }));
93+
t.true(
94+
using({
95+
etagMatches: [{ type: "strong", value: etag }],
96+
uploadedBefore: pastDate,
97+
})
98+
);
6899
// `etagDoesNotMatch` pass makes `uploadedAfter` pass
69-
t.true(using({ etagDoesNotMatch: badEtag, uploadedAfter: futureDate }));
100+
t.true(
101+
using({
102+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
103+
uploadedAfter: futureDate,
104+
})
105+
);
70106
t.true(
71107
using({
72108
// `etagMatches` pass makes `uploadedBefore` pass
73-
etagMatches: etag,
109+
etagMatches: [{ type: "strong", value: etag }],
74110
uploadedBefore: pastDate,
75111
// `etagDoesNotMatch` pass makes `uploadedAfter` pass
76-
etagDoesNotMatch: badEtag,
112+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
77113
uploadedAfter: futureDate,
78114
})
79115
);
80116
t.true(
81117
using({
82118
uploadedBefore: futureDate,
83119
// `etagDoesNotMatch` pass makes `uploadedAfter` pass
84-
etagDoesNotMatch: badEtag,
120+
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
85121
uploadedAfter: futureDate,
86122
})
87123
);
88124
t.true(
89125
using({
90126
uploadedAfter: pastDate,
91127
// `etagMatches` pass makes `uploadedBefore` pass
92-
etagMatches: etag,
128+
etagMatches: [{ type: "strong", value: etag }],
93129
uploadedBefore: pastDate,
94130
})
95131
);
96132

97133
// Check missing metadata fails with either `etagMatches` and `uploadedAfter`
98-
t.false(usingMissing({ etagMatches: etag }));
134+
t.false(usingMissing({ etagMatches: [{ type: "strong", value: etag }] }));
99135
t.false(usingMissing({ uploadedAfter: pastDate }));
100-
t.false(usingMissing({ etagMatches: etag, uploadedAfter: pastDate }));
101-
t.true(usingMissing({ etagDoesNotMatch: etag }));
136+
t.false(
137+
usingMissing({
138+
etagMatches: [{ type: "strong", value: etag }],
139+
uploadedAfter: pastDate,
140+
})
141+
);
142+
t.true(usingMissing({ etagDoesNotMatch: [{ type: "strong", value: etag }] }));
102143
t.true(usingMissing({ uploadedBefore: pastDate }));
103-
t.true(usingMissing({ etagDoesNotMatch: etag, uploadedBefore: pastDate }));
104-
t.false(usingMissing({ etagMatches: etag, uploadedBefore: pastDate }));
105-
t.false(usingMissing({ etagDoesNotMatch: etag, uploadedAfter: pastDate }));
144+
t.true(
145+
usingMissing({
146+
etagDoesNotMatch: [{ type: "strong", value: etag }],
147+
uploadedBefore: pastDate,
148+
})
149+
);
150+
t.false(
151+
usingMissing({
152+
etagMatches: [{ type: "strong", value: etag }],
153+
uploadedBefore: pastDate,
154+
})
155+
);
156+
t.false(
157+
usingMissing({
158+
etagDoesNotMatch: [{ type: "strong", value: etag }],
159+
uploadedAfter: pastDate,
160+
})
161+
);
106162

107163
// Check with second granularity
108164
const justPastDate = new Date(uploadedDate.getTime() - 250);

0 commit comments

Comments
 (0)
This repository has been archived.