unjs / ipx

🖼️ High performance, secure and easy-to-use image optimizer.
MIT License
1.99k stars 66 forks source link

Support data URIs #244

Open tunnckoCore opened 1 week ago

tunnckoCore commented 1 week ago

Describe the feature

It would be pretty basic option, basically skipping the url.hostname check in validateId or the validation altogther https://github.com/unjs/ipx/blob/main/src/storage/http.ts#99

Everything else can stay the same and actually work, because Fetch can fetch data URLs.

Additional information

example URI

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAD80lEQVR4nO2XT2gUVxzHP5PNbjaxkVhT0+mCZBvdQ/MH9iCiNeaQUBURWoQGtqGnHkp7K6UIIW2htASE0FMPBaEHs4JSUEFNk9hAFhXpQbD2kvVPWF2G6FpIaeN2k93pYfJmZ2ZnZ98kK/Xg97L7/uy8z3x/v/feb5WUrui8wGr4vwFq6SXgZvUScLNq9PuDI+GAa39htQhAKGiMX8kXN4FVljSgANOO9gGgXrrNrdOfARAfmeDpu3Fz7u3flzgUXrKBWl/MD7wU4JFwgF92dtDX21EBF7p4v2J+X28HWm+HDfTyl8OosQjxkQlpOPCRgwLOKje4ClDheCziC0zIdw5a3XMqOp9x7Q+3ttE9fJL0B8f9LlcbUITXKeFe99nzhFvbiM5nuLCtyehLKPyRLN+g3QmFyHiQt85eRKk3IJTDG53PEG5tIz4yYUI/OvG6Daaapne+BsA7mSdAHTfJlXyR7YFbbH11Oxe2Na27U+no22mNV5rWU3rcMTiO+ULZEzuIhB8zlV+tD+BgAOa63qjoP5RZssGkz33u+Zz4yAQ6DWTZIQUmpHiVW4MBeDwWMUP43vIqK8t/AlTdKF7SFrIcHD3DlqbG+jlYDzAhcdTIwoGEg2BcX37AtIWsDcjaFi6mdLk62fOgvrq+2fy69pd2mo9+umZrHxydNCG3hOQPm+dSzRw9XvBs/1OQ/5fxwpdbdQHUFrJmngHczY1x6bv3Adi15zBEr6Ev/mqOb20JSj/b913sJrdCYNeewwAkPhk1+5I/fAvASuF76WfXdDBQwuaOU073rHDVtLa2JoknATitw9BYsuq4GovYHHTCCdfEp19J56AfF5XOIdv4mcs3ze/Nbw5Jn4EgmYNXizAYSDL7TcI135x9+uKsDVJfnAUgNXmKfEmaDahxkwj1K8bBGmrQq0JaVS0HlU5/7oFEiAUcQHNjiKGxJN3DJz1Dfve3KVs7NXlqQ3Ag4aAVEDAXOdYCK//i6qgV/tjXkyyvQkr3W0tLAgIMBBRKltyxOmENf6FU7k+f+8KEFffwRiS1SUoeiW2DbYSZqRl6enpQVdU2T9M05g4MABBtbweg8/zPqKpqvsyGAVO6bgt1v6K45lOwIciTjz/lx/wzZmYMUKG5AwOk88+MRi4HwN937hjwHvksFWIBFVICFPSiCe0+T+fG3v08yOVMoN3hZqLt7Uw/eshX2Ycyy/kHFJBWuUEKQD/yCnVdigWrUrrCPuV6VcgH6+G1yivUvhwEeReTXTFzMzi17+Z16fWeC6AxzxsSDNBa56PvgtUJ5AQuz1P48F7aNaRCN/bup7+GP5uuqFuC1SHndEjcW/CETHbFPCH/A4GLqlx3WtZcAAAAAElFTkSuQmCC

And of course, there could be URL limit hit this way, but for small images it's okay (as above)

Other way would be to expose a similar thing programmatically accepting buffers.

tunnckoCore commented 1 week ago

My current workaround is to detect if it's data: url, download it to the assets folder and rewrite the url of the request as it's trying local file and deleting that file after that.

import { createApp, toWebHandler } from "h3";
import {
  createIPX,
  createIPXH3App,
  createIPXH3Handler,
  ipxFSStorage,
  ipxHttpStorage,
} from "ipx";
import { rm as remove } from "node:fs/promises";

const ipx = createIPX({
  storage: ipxFSStorage({ dir: "./assets" }),
  httpStorage: ipxHttpStorage({ allowAllDomains: true, maxAge: 60 * 60 * 24 }),
});

export const app = createApp().use("/optimize", createIPXH3Handler(ipx));
const handler = toWebHandler(app);

Bun.serve({
  port: 3000,
  async fetch(req) {
    let url = new URL(req.url);

    if (url.pathname.startsWith("/optimize")) {
      const [_, seg] = url.pathname.split("/optimize/");
      const index = seg.indexOf("data");
      const fpath = index === -1 ? seg : seg.slice(index);

      let digest;

      // download it locally to the assets folder, then delete
      // at least until https://github.com/unjs/ipx/issues/244
      if (fpath.startsWith("data:")) {
        const buf = await fetch(fpath).then((res) => res.arrayBuffer());
        digest = await createDigest(new Uint8Array(buf));
        await Bun.write(`./assets/${digest}`, buf);

        const resp = await handler(
          new Request(
            `${url.origin}/optimize/${seg.slice(0, index - 1)}/${digest}`,
            req,
          ),
        );

        remove(`./assets/${digest}`);

        return resp;
      }
      return handler(req);
    }
    return new Response("Not found", { status: 404 });
  },
});

async function createDigest(
  msg: string | Uint8Array,
  algo: "SHA-1" | "SHA-256" | "SHA-512" = "SHA-256",
) {
  const data = typeof msg === "string" ? new TextEncoder().encode(msg) : msg;
  const hashBuffer = await crypto.subtle.digest(algo, data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return hashHex;
}