Skip to content

Commit

Permalink
Add support for multiple, weak, wildcard etags in R2 gateway
Browse files Browse the repository at this point in the history
cloudflare/workerd#563 added support to R2 bindings for these
  • Loading branch information
mrbbot committed Oct 31, 2023
1 parent 12da40c commit e2d12b1
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 31 deletions.
12 changes: 10 additions & 2 deletions packages/miniflare/src/plugins/r2/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,20 @@ export const R2RangeSchema = z.object({
});
export type R2Range = z.infer<typeof R2RangeSchema>;

export const R2EtagSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("strong"), value: z.string() }),
z.object({ type: z.literal("weak"), value: z.string() }),
z.object({ type: z.literal("wildcard") }),
]);
export type R2Etag = z.infer<typeof R2EtagSchema>;
export const R2EtagMatchSchema = R2EtagSchema.array().min(1).optional();

// For more information, refer to https://datatracker.ietf.org/doc/html/rfc7232
export const R2ConditionalSchema = z.object({
// Performs the operation if the object's ETag matches the given string
etagMatches: z.ostring(), // "If-Match"
etagMatches: R2EtagMatchSchema, // "If-Match"
// Performs the operation if the object's ETag does NOT match the given string
etagDoesNotMatch: z.ostring(), // "If-None-Match"
etagDoesNotMatch: R2EtagMatchSchema, // "If-None-Match"
// Performs the operation if the object was uploaded BEFORE the given date
uploadedBefore: DateSchema.optional(), // "If-Unmodified-Since"
// Performs the operation if the object was uploaded AFTER the given date
Expand Down
24 changes: 21 additions & 3 deletions packages/miniflare/src/plugins/r2/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
PreconditionFailed,
} from "./errors";
import { R2Object } from "./r2Object";
import { R2Conditional, R2GetOptions } from "./schemas";
import { R2Conditional, R2Etag, R2GetOptions } from "./schemas";

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

function includesEtag(
conditions: R2Etag[],
etag: string,
comparison: "strong" | "weak"
) {
// Adapted from internal R2 gateway implementation.
for (const condition of conditions) {
if (condition.type === "wildcard") return true;
if (condition.value === etag) {
if (condition.type === "strong" || comparison === "weak") return true;
}
}
return false;
}

// Returns `true` iff the condition passed
/** @internal */
export function _testR2Conditional(
Expand All @@ -43,9 +58,12 @@ export function _testR2Conditional(
}

const { etag, uploaded: lastModifiedRaw } = metadata;
const ifMatch = cond.etagMatches === undefined || cond.etagMatches === etag;
const ifMatch =
cond.etagMatches === undefined ||
includesEtag(cond.etagMatches, etag, "strong");
const ifNoneMatch =
cond.etagDoesNotMatch === undefined || cond.etagDoesNotMatch !== etag;
cond.etagDoesNotMatch === undefined ||
!includesEtag(cond.etagDoesNotMatch, etag, "weak");

const maybeTruncate = cond.secondsGranularity ? truncateToSeconds : identity;
const lastModified = maybeTruncate(lastModifiedRaw);
Expand Down
108 changes: 82 additions & 26 deletions packages/miniflare/test/plugins/r2/validator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { R2Conditional } from "@cloudflare/workers-types/experimental";
import { R2Object, _testR2Conditional } from "@miniflare/tre";
import { R2Conditional, R2Object, _testR2Conditional } from "@miniflare/tre";
import test from "ava";

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

// Check single conditions
t.true(using({ etagMatches: etag }));
t.false(using({ etagMatches: badEtag }));
t.true(using({ etagMatches: [{ type: "strong", value: etag }] }));
t.false(using({ etagMatches: [{ type: "strong", value: badEtag }] }));

t.true(using({ etagDoesNotMatch: badEtag }));
t.false(using({ etagDoesNotMatch: etag }));
t.true(using({ etagDoesNotMatch: [{ type: "strong", value: badEtag }] }));
t.false(using({ etagDoesNotMatch: [{ type: "strong", value: etag }] }));

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

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

// Check with weaker etags
t.false(using({ etagMatches: [{ type: "weak", value: etag }] }));
t.false(using({ etagDoesNotMatch: [{ type: "weak", value: etag }] }));
t.true(using({ etagDoesNotMatch: [{ type: "weak", value: badEtag }] }));
t.true(using({ etagMatches: [{ type: "wildcard" }] }));
t.false(using({ etagDoesNotMatch: [{ type: "wildcard" }] }));

// Check multiple conditions that evaluate to false
t.false(using({ etagMatches: etag, etagDoesNotMatch: etag }));
t.false(using({ etagMatches: etag, uploadedAfter: futureDate }));
t.false(
using({
etagMatches: [{ type: "strong", value: etag }],
etagDoesNotMatch: [{ type: "strong", value: etag }],
})
);
t.false(
using({
etagMatches: [{ type: "strong", value: etag }],
uploadedAfter: futureDate,
})
);
t.false(
using({
// `etagMatches` pass makes `uploadedBefore` pass, but `uploadedAfter` fails
etagMatches: etag,
etagMatches: [{ type: "strong", value: etag }],
uploadedAfter: futureDate,
uploadedBefore: pastDate,
})
);
t.false(using({ etagDoesNotMatch: badEtag, uploadedBefore: pastDate }));
t.false(
using({
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
uploadedBefore: pastDate,
})
);
t.false(
using({
// `etagDoesNotMatch` pass makes `uploadedAfter` pass, but `uploadedBefore` fails
etagDoesNotMatch: badEtag,
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
uploadedAfter: futureDate,
uploadedBefore: pastDate,
})
);
t.false(
using({
etagMatches: badEtag,
etagDoesNotMatch: badEtag,
etagMatches: [{ type: "strong", value: badEtag }],
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
uploadedAfter: pastDate,
uploadedBefore: futureDate,
})
);

// Check multiple conditions that evaluate to true
t.true(using({ etagMatches: etag, etagDoesNotMatch: badEtag }));
t.true(
using({
etagMatches: [{ type: "strong", value: etag }],
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
})
);
// `etagMatches` pass makes `uploadedBefore` pass
t.true(using({ etagMatches: etag, uploadedBefore: pastDate }));
t.true(
using({
etagMatches: [{ type: "strong", value: etag }],
uploadedBefore: pastDate,
})
);
// `etagDoesNotMatch` pass makes `uploadedAfter` pass
t.true(using({ etagDoesNotMatch: badEtag, uploadedAfter: futureDate }));
t.true(
using({
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
uploadedAfter: futureDate,
})
);
t.true(
using({
// `etagMatches` pass makes `uploadedBefore` pass
etagMatches: etag,
etagMatches: [{ type: "strong", value: etag }],
uploadedBefore: pastDate,
// `etagDoesNotMatch` pass makes `uploadedAfter` pass
etagDoesNotMatch: badEtag,
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
uploadedAfter: futureDate,
})
);
t.true(
using({
uploadedBefore: futureDate,
// `etagDoesNotMatch` pass makes `uploadedAfter` pass
etagDoesNotMatch: badEtag,
etagDoesNotMatch: [{ type: "strong", value: badEtag }],
uploadedAfter: futureDate,
})
);
t.true(
using({
uploadedAfter: pastDate,
// `etagMatches` pass makes `uploadedBefore` pass
etagMatches: etag,
etagMatches: [{ type: "strong", value: etag }],
uploadedBefore: pastDate,
})
);

// Check missing metadata fails with either `etagMatches` and `uploadedAfter`
t.false(usingMissing({ etagMatches: etag }));
t.false(usingMissing({ etagMatches: [{ type: "strong", value: etag }] }));
t.false(usingMissing({ uploadedAfter: pastDate }));
t.false(usingMissing({ etagMatches: etag, uploadedAfter: pastDate }));
t.true(usingMissing({ etagDoesNotMatch: etag }));
t.false(
usingMissing({
etagMatches: [{ type: "strong", value: etag }],
uploadedAfter: pastDate,
})
);
t.true(usingMissing({ etagDoesNotMatch: [{ type: "strong", value: etag }] }));
t.true(usingMissing({ uploadedBefore: pastDate }));
t.true(usingMissing({ etagDoesNotMatch: etag, uploadedBefore: pastDate }));
t.false(usingMissing({ etagMatches: etag, uploadedBefore: pastDate }));
t.false(usingMissing({ etagDoesNotMatch: etag, uploadedAfter: pastDate }));
t.true(
usingMissing({
etagDoesNotMatch: [{ type: "strong", value: etag }],
uploadedBefore: pastDate,
})
);
t.false(
usingMissing({
etagMatches: [{ type: "strong", value: etag }],
uploadedBefore: pastDate,
})
);
t.false(
usingMissing({
etagDoesNotMatch: [{ type: "strong", value: etag }],
uploadedAfter: pastDate,
})
);

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

0 comments on commit e2d12b1

Please sign in to comment.