Skip to content
Open
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
1 change: 1 addition & 0 deletions kits/storage-resize-images/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- fix: restore the extension's default for an omitted `deleteOriginal`. The extension mapped every `DELETE_ORIGINAL_FILE` value other than `"true"`/`"false"` (unset included) to delete-on-success; the kit resolved an omitted `deleteOriginal` to never-delete. `resolveResizeImagesConfig` now resolves an omitted `deleteOriginal` to delete-on-success, matching the extension. Only library consumers who omit the field are affected; deploys are unaffected (the Firebase CLI writes the declared `"false"` default into `.env`, and an env var that is present resolves as before). Pass `deleteOriginal: "false"` to keep originals.
- fix: restore the `us-central1` content-filter fallback. `checkImageContent` threw `FUNCTION_REGION is required for Vertex AI filtering.` when no region was available; the extension fell back to `us-central1`. The Vertex AI call now uses the function's region when known and `us-central1` otherwise, matching the extension. Normal CLI deploys were unaffected (the Firebase CLI sets `FUNCTION_REGION` on deployed functions); the throw was reachable for library consumers, emulator runs, and hand-rolled environments.
- Initial release of kit, see README for differences between the legacy extension and this kit
16 changes: 16 additions & 0 deletions kits/storage-resize-images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ loads them at deploy time and prompts for any required values that are missing.
| `customFilterPrompt` | `CUSTOM_FILTER_PROMPT` | no | (empty) | Custom filter prompt |
| `placeholderImagePath` | `PLACEHOLDER_IMAGE_PATH` | no | (empty) | Placeholder for filtered images |

The `deleteOriginal` default above is what the CLI writes into `.env` at deploy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, and cheap to fix in this PR. Reading firebase-tools 15.29.0 (lib/deploy/functions/params.js, resolveParams), a param absent from .env is prompted for with its declared default and the resolved value is injected into the function environment, and a non-interactive deploy fails outright instead. Nothing is written back into .env, so the deploy-safety conclusion holds but by a different route than this sentence describes. Also, the table above still lists the deleteOriginal default as false, so a reader who only scans the table takes away the opposite of the new behaviour; a footnote on that row pointing down here would close the gap. This comes from reading the dependency source, not from a live deploy.

time. Omitting `deleteOriginal` when calling `resolveResizeImagesConfig`
directly deletes the original on a successful resize, matching how the
extension treated an unset `DELETE_ORIGINAL_FILE`; pass `"false"` to keep
originals.

## Multiple instances

To resize images from several buckets or pipelines, add one entry per instance
Expand Down Expand Up @@ -180,6 +186,16 @@ the extension is installed. A malformed value fails the deploy with
`Invalid includePathList: must be a comma-separated list of absolute path
values.` rather than being rejected by an install prompt.

### An omitted `isAnimated` keeps animation

The extension's config parser had a bug: `overrideIsAnimated === "true" ||
undefined` never evaluated the intended unset check, so an unset `IS_ANIMATED`
produced first-frame-only output even though the parameter's declared default
was `true`. The kit deliberately fixes this rather than reproducing it: an
omitted `isAnimated` resolves to `true`, the default the extension intended.
Deploys are unaffected either way, since the CLI writes `IS_ANIMATED=true`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, and also cheap here. isAnimated is a defineBoolean, and in firebase-functions BooleanParam.runtimeValue() reads !!process.env[name] && process.env[name] === "true", so when IS_ANIMATED is not present in the runtime environment configFromEnv() yields false and animation is dropped. The fix applies to callers who omit the field programmatically, so scoping the section to that path rather than to omission in general would match what the code does. Read from the dependency source, not exercised against a deploy.

into `.env`; pass `isAnimated: false` for first-frame-only output.

### No backfill

There is no function to resize images that already exist in the bucket. The
Expand Down
1 change: 0 additions & 1 deletion kits/storage-resize-images/src/export-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ function deleteOriginalFile(
return DELETE_IMAGE.always;
case false:
case "false":
case undefined:
return DELETE_IMAGE.never;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With case undefined gone, a caller who omits deleteOriginal resolves to onSuccess, and resolveResizeImagesConfig is exported from src/lib.ts, so a library consumer writing resolveResizeImagesConfig({ bucket, sizes }) now has every original deleted after a successful resize where the previous kit release kept it. The sharpest case is the content filter: with no failedImagesPath set, handleFailedImage stores nothing, the placeholder resize succeeds, and the flagged original is deleted with no copy left anywhere, which is what the new test at tests/handlers.test.ts:418 pins. One thing worth weighing in the parity argument: extension.yaml declares DELETE_ORIGINAL_FILE as required: true with default: false, so the unset arm did not run in a real install. This needs a decision before merge, either defaulting the optional field to "false" so omission stays safe, or making deleteOriginal required so an absent field cannot delete data.

default:
return DELETE_IMAGE.onSuccess;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. The default arm now takes both omission and every unrecognized value into the destructive branch. DELETE_ORIGINAL_FILE is a plain defineString (src/config.ts:90) with no validation, so a hand-written .env carrying False, no, or 0 resolves to delete-on-success. Throwing on anything that is not true, false, or on_success would keep whatever you decide for omission while making a typo fail loudly rather than delete originals.

Expand Down
14 changes: 12 additions & 2 deletions kits/storage-resize-images/tests/export-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,22 @@ describe("resolveResizeImagesConfig", () => {
).toEqual(DELETE_IMAGE.never);
});

test("an unset deleteOriginal never deletes", () => {
test("an unset deleteOriginal deletes on success, matching the extension", () => {
// The extension mapped every DELETE_ORIGINAL_FILE value other than
// "true"/"false" (unset included) to onSuccess.
const resolved = resolveResizeImagesConfig({
...baseConfig,
deleteOriginal: undefined,
});
expect(resolved.deleteOriginalFile).toEqual(DELETE_IMAGE.never);
expect(resolved.deleteOriginalFile).toEqual(DELETE_IMAGE.onSuccess);
});

test("an empty-string deleteOriginal (partial env) deletes on success", () => {
const resolved = resolveResizeImagesConfig({
...baseConfig,
deleteOriginal: "" as ResizeImagesConfig["deleteOriginal"],
});
expect(resolved.deleteOriginalFile).toEqual(DELETE_IMAGE.onSuccess);
});

test("splits a comma-separated sizes string", () => {
Expand Down
75 changes: 69 additions & 6 deletions kits/storage-resize-images/tests/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { checkImageContent } from "../src/content-filter";
import * as events from "../src/events";
import {
DELETE_IMAGE,
type ResizeImagesConfig,
type ResolvedResizeImagesConfig,
resolveResizeImagesConfig,
} from "../src/export-config";
Expand All @@ -97,16 +98,22 @@ const mock = <T>(fn: T) => fn as unknown as ReturnType<typeof vi.fn>;

const bucketStub = {};

// deleteOriginal is pinned: an omitted value resolves to on_success, and
// these tests exercise handler logic, not the resolver's defaults.
const baseInput: ResizeImagesConfig = {
bucket: "demo-bucket",
sizes: "200x200",
region: "us-central1",
deleteOriginal: "false",
};

function makeCtx(
overrides: Partial<ResolvedResizeImagesConfig> = {}
overrides: Partial<ResolvedResizeImagesConfig> = {},
input: ResizeImagesConfig = baseInput
): HandlerContext {
return {
config: {
...resolveResizeImagesConfig({
bucket: "demo-bucket",
sizes: "200x200",
region: "us-central1",
}),
...resolveResizeImagesConfig(input),
...overrides,
},
storage: {
Expand Down Expand Up @@ -390,6 +397,62 @@ describe("generateResizedImageHandler", () => {
expect(deleteRemoteFile).not.toHaveBeenCalled();
});

test("an omitted deleteOriginal deletes the original after a successful run", async () => {
// The extension resolved an unset DELETE_ORIGINAL_FILE to on_success.
const remoteFile = { delete: vi.fn() };
mock(downloadOriginalFile).mockResolvedValue(["/tmp/test.jpg", remoteFile]);
const ctx = makeCtx(
{},
{ bucket: "demo-bucket", sizes: "200x200", region: "us-central1" }
);

await generateResizedImageHandler(mockObject, ctx, false);

expect(deleteRemoteFile).toHaveBeenCalledWith(
remoteFile,
"images/test.jpg"
);
expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
});

test("an omitted deleteOriginal deletes a filter-blocked original once its placeholder resizes", async () => {
// Matches the extension: the blocked original is replaced and then removed
// under on_success, unless failedImagesPath stored a copy first.
const remoteFile = { delete: vi.fn() };
mock(downloadOriginalFile).mockResolvedValue(["/tmp/test.jpg", remoteFile]);
mock(checkImageContent).mockResolvedValue(false);
const ctx = makeCtx(
{},
{ bucket: "demo-bucket", sizes: "200x200", region: "us-central1" }
);

await generateResizedImageHandler(mockObject, ctx, false);

expect(deleteRemoteFile).toHaveBeenCalledWith(
remoteFile,
"images/test.jpg"
);
expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
});

test("an omitted deleteOriginal keeps the original on a failed run", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this one passes with the source change reverted, because an omitted deleteOriginal then resolves to never and deleteRemoteFile is not called either way. It also lands on the same handler branch as the test at line 357, which reaches on_success through an explicit override. Dropping it, or asserting something only the resolved on_success path can produce, would keep the suite discriminating. Separately, the comment at line 420 mentions failedImagesPath storing a copy first, and that path is not exercised there since handleFailedImage is mocked and no failedImagesPath is configured.

mock(downloadOriginalFile).mockResolvedValue([
"/tmp/test.jpg",
{ delete: vi.fn() },
]);
mock(resizeImages).mockResolvedValue([
{ status: "fulfilled", value: { success: false } },
]);
const ctx = makeCtx(
{},
{ bucket: "demo-bucket", sizes: "200x200", region: "us-central1" }
);

await generateResizedImageHandler(mockObject, ctx, false);

expect(deleteRemoteFile).not.toHaveBeenCalled();
});

test("cleans up the temp files it created", async () => {
const ctx = makeCtx();
mock(checkImageContent).mockResolvedValue(false);
Expand Down
Loading