From af8adf9ce745743484ebeea3ef98aeab5316b182 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Sun, 30 Jul 2023 18:27:45 +0200 Subject: [PATCH] feat: correctly cache images with redis --- components/Card.tsx | 29 ++++++++ components/RecipeCard.tsx | 37 ++++------ components/RecipeHero.tsx | 20 ++++-- deno.json | 1 + fresh.gen.ts | 12 ++-- lib/cache.ts | 29 -------- lib/cache/cache.ts | 42 +++++++++++ lib/cache/documents.ts | 57 +++++++++++++++ lib/cache/image.ts | 63 +++++++++++++++++ lib/documents.ts | 18 ++++- lib/hash.ts | 9 +++ lib/recipes.ts | 7 +- .../images/[image].ts => images/index.ts} | 65 +++++++++--------- routes/index.tsx | 24 +++---- routes/recipes/index.tsx | 13 +++- static/global.css | 16 ++++- static/grainy-gradient.png | Bin 0 -> 31239 bytes 17 files changed, 321 insertions(+), 121 deletions(-) create mode 100644 components/Card.tsx delete mode 100644 lib/cache.ts create mode 100644 lib/cache/cache.ts create mode 100644 lib/cache/documents.ts create mode 100644 lib/cache/image.ts create mode 100644 lib/hash.ts rename routes/api/{recipes/images/[image].ts => images/index.ts} (78%) create mode 100644 static/grainy-gradient.png diff --git a/components/Card.tsx b/components/Card.tsx new file mode 100644 index 0000000..2b19fa9 --- /dev/null +++ b/components/Card.tsx @@ -0,0 +1,29 @@ +import { Recipe } from "../lib/recipes.ts"; + +export function Card( + { link, title, image }: { link?: string; title?: string; image?: string }, +) { + return ( + +
+
+ {/* Recipe Card content */} +
+
+ {title} +
+
+
+ + ); +} diff --git a/components/RecipeCard.tsx b/components/RecipeCard.tsx index a67349f..37a60ff 100644 --- a/components/RecipeCard.tsx +++ b/components/RecipeCard.tsx @@ -1,29 +1,18 @@ -import { Recipe } from "../lib/recipes.ts"; +import { Card } from "@components/Card.tsx"; +import { Recipe } from "@lib/recipes.ts"; export function RecipeCard({ recipe }: { recipe: Recipe }) { + const { meta: { image = "Recipes/images/placeholder.jpg" } = {} } = recipe; + + const imageUrl = image.startsWith("Recipes/images/") + ? `/api/images?image=${image}&width=200&height=200` + : image; + return ( - -
-
- {/* Recipe Card content */} -
-
- {recipe.name} -
-
-
- + ); } diff --git a/components/RecipeHero.tsx b/components/RecipeHero.tsx index 244bca0..b939e4d 100644 --- a/components/RecipeHero.tsx +++ b/components/RecipeHero.tsx @@ -1,10 +1,16 @@ -import { Recipe } from "../lib/recipes.ts"; +import { Recipe } from "@lib/recipes.ts"; export function RecipeHero({ recipe }: { recipe: Recipe }) { + const { meta: { image = "Recipes/images/placeholder.jpg" } = {} } = recipe; + + const imageUrl = image.startsWith("Recipes/images/") + ? `/api/images?image=${image}&width=800` + : image; + return (
Recipe Banner @@ -18,11 +24,11 @@ export function RecipeHero({ recipe }: { recipe: Recipe }) {
-
-

+
+

{recipe.name}

diff --git a/deno.json b/deno.json index d38f81b..ea81c4f 100755 --- a/deno.json +++ b/deno.json @@ -2,6 +2,7 @@ "lock": false, "tasks": { "start": "deno run -A --watch=static/,routes/ dev.ts", + "debug": "deno run --inspect-wait -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update ." }, "lint": { diff --git a/fresh.gen.ts b/fresh.gen.ts index 34517ce..4ffeaa6 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -4,9 +4,9 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; -import * as $2 from "./routes/api/index.ts"; -import * as $3 from "./routes/api/recipes/[name].ts"; -import * as $4 from "./routes/api/recipes/images/[image].ts"; +import * as $2 from "./routes/api/images/index.ts"; +import * as $3 from "./routes/api/index.ts"; +import * as $4 from "./routes/api/recipes/[name].ts"; import * as $5 from "./routes/api/recipes/index.ts"; import * as $6 from "./routes/index.tsx"; import * as $7 from "./routes/recipes/[name].tsx"; @@ -17,9 +17,9 @@ const manifest = { routes: { "./routes/_404.tsx": $0, "./routes/_app.tsx": $1, - "./routes/api/index.ts": $2, - "./routes/api/recipes/[name].ts": $3, - "./routes/api/recipes/images/[image].ts": $4, + "./routes/api/images/index.ts": $2, + "./routes/api/index.ts": $3, + "./routes/api/recipes/[name].ts": $4, "./routes/api/recipes/index.ts": $5, "./routes/index.tsx": $6, "./routes/recipes/[name].tsx": $7, diff --git a/lib/cache.ts b/lib/cache.ts deleted file mode 100644 index 32becce..0000000 --- a/lib/cache.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { connect } from "https://deno.land/x/redis/mod.ts"; - -const REDIS_HOST = Deno.env.get("REDIS_HOST"); -const REDIS_PASS = Deno.env.get("REDIS_PASS"); -const REDIS_PORT = Deno.env.get("REDIS_PORT"); - -async function createCache() { - if (REDIS_HOST && REDIS_PASS) { - const client = await connect({ - password: REDIS_PASS, - hostname: REDIS_HOST, - port: REDIS_PORT || 6379, - }); - console.log("COnnected to redis"); - return client; - } - - return new Map(); -} - -const cache = await createCache(); - -export async function get(id: string) { - return await cache.get(id); -} - -export async function set(id: string, content: any) { - return await cache.set(id, content); -} diff --git a/lib/cache/cache.ts b/lib/cache/cache.ts new file mode 100644 index 0000000..c5aa77d --- /dev/null +++ b/lib/cache/cache.ts @@ -0,0 +1,42 @@ +import { + connect, + Redis, + RedisConnectOptions, + RedisValue, +} from "https://deno.land/x/redis@v0.31.0/mod.ts"; + +const REDIS_HOST = Deno.env.get("REDIS_HOST"); +const REDIS_PASS = Deno.env.get("REDIS_PASS") || ""; +const REDIS_PORT = Deno.env.get("REDIS_PORT"); + +async function createCache(): Promise | Redis> { + if (REDIS_HOST) { + const conf: RedisConnectOptions = { + hostname: REDIS_HOST, + port: REDIS_PORT || 6379, + }; + if (REDIS_PASS) { + conf.password = REDIS_PASS; + } + const client = await connect(conf); + console.log("Connected to redis"); + return client; + } + + return new Map(); +} + +const cache = await createCache(); + +export async function get(id: string, binary = false) { + if (binary && !(cache instanceof Map)) { + return await cache.sendCommand("GET", [id], { + returnUint8Arrays: true, + }) as T; + } + return await cache.get(id) as T; +} + +export async function set(id: string, content: T) { + return await cache.set(id, content); +} diff --git a/lib/cache/documents.ts b/lib/cache/documents.ts new file mode 100644 index 0000000..8c4754c --- /dev/null +++ b/lib/cache/documents.ts @@ -0,0 +1,57 @@ +import { Document } from "@lib/documents.ts"; +import * as cache from "@lib/cache/cache.ts"; + +type DocumentsCache = { + lastUpdated: number; + documents: Document[]; +}; + +const CACHE_INTERVAL = 5000; // 5 seconds; +const CACHE_KEY = "documents"; + +export async function getDocuments() { + const docs = await cache.get(CACHE_KEY); + if (!docs) return; + + if (Date.now() > docs.lastUpdated + CACHE_INTERVAL) { + return; + } + + return docs.documents; +} + +export function setDocuments(documents: Document[]) { + return cache.set( + CACHE_KEY, + JSON.stringify({ + lastUpdated: Date.now(), + documents, + }), + ); +} + +type DocumentCache = { + lastUpdated: number; + content: string; +}; + +export async function getDocument(id: string) { + const doc = await cache.get(CACHE_KEY + "/" + id); + if (!doc) return; + + if (Date.now() > doc.lastUpdated + CACHE_INTERVAL) { + return; + } + + return doc.content; +} + +export async function setDocument(id: string, content: string) { + await cache.set( + CACHE_KEY + "/" + id, + JSON.stringify({ + lastUpdated: Date.now(), + content, + }), + ); +} diff --git a/lib/cache/image.ts b/lib/cache/image.ts new file mode 100644 index 0000000..abbb4d1 --- /dev/null +++ b/lib/cache/image.ts @@ -0,0 +1,63 @@ +import { hash } from "@lib/hash.ts"; +import * as cache from "@lib/cache/cache.ts"; + +type ImageCacheOptions = { + url: string; + width: number; + height: number; + mediaType?: string; +}; + +const CACHE_KEY = "images"; + +function getCacheKey({ url: _url, width, height }: ImageCacheOptions) { + const url = new URL(_url); + + return `${CACHE_KEY}/${url.hostname}/${url.pathname}/${width}/${height}` + .replace( + "//", + "/", + ); +} + +export async function getImage({ url, width, height }: ImageCacheOptions) { + const cacheKey = getCacheKey({ url, width, height }); + + const pointerCacheRaw = await cache.get(cacheKey); + if (!pointerCacheRaw) return; + + const pointerCache = typeof pointerCacheRaw === "string" + ? JSON.parse(pointerCacheRaw) + : pointerCacheRaw; + + const imageContent = await cache.get(pointerCache.id, true); + if (!imageContent) return; + + return { + ...pointerCache, + buffer: imageContent, + }; +} + +export async function setImage( + buffer: Uint8Array, + { url, width, height, mediaType }: ImageCacheOptions, +) { + const clone = new Uint8Array(buffer); + + const cacheKey = getCacheKey({ url, width, height }); + const pointerId = await hash(cacheKey); + + await cache.set(pointerId, clone); + + await cache.set( + cacheKey, + JSON.stringify({ + id: pointerId, + url, + width, + height, + mediaType, + }), + ); +} diff --git a/lib/documents.ts b/lib/documents.ts index 6e3bd85..c74c2ca 100644 --- a/lib/documents.ts +++ b/lib/documents.ts @@ -2,6 +2,7 @@ import { unified } from "npm:unified"; import remarkParse from "npm:remark-parse"; import remarkFrontmatter from "https://esm.sh/remark-frontmatter@4"; import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts"; +import * as cache from "@lib/cache/documents.ts"; const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER"); @@ -18,6 +19,9 @@ export function parseFrontmatter(yaml: string) { } export async function getDocuments(): Promise { + const cachedDocuments = await cache.getDocuments(); + if (cachedDocuments) return cachedDocuments; + const headers = new Headers(); headers.append("Accept", "application/json"); @@ -25,12 +29,22 @@ export async function getDocuments(): Promise { headers: headers, }); - return response.json(); + const documents = await response.json(); + cache.setDocuments(documents); + + return documents; } export async function getDocument(name: string): Promise { + const cachedDocument = await cache.getDocument(name); + if (cachedDocument) return cachedDocument; + const response = await fetch(SILVERBULLET_SERVER + "/" + name); - return await response.text(); + const text = await response.text(); + + cache.setDocument(name, text); + + return text; } export function parseDocument(doc: string) { diff --git a/lib/hash.ts b/lib/hash.ts new file mode 100644 index 0000000..3db7dff --- /dev/null +++ b/lib/hash.ts @@ -0,0 +1,9 @@ +export async function hash(message: string) { + const data = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join( + "", + ); + return hashHex; +} diff --git a/lib/recipes.ts b/lib/recipes.ts index 2fa7f6d..1a87d03 100644 --- a/lib/recipes.ts +++ b/lib/recipes.ts @@ -4,7 +4,7 @@ import { getTextOfRange, parseDocument, parseFrontmatter, -} from "./documents.ts"; +} from "@lib/documents.ts"; import { parseIngredient } from "npm:parse-ingredient"; @@ -161,10 +161,7 @@ export function parseRecipe(original: string, id: string): Recipe { return { id, - meta: { - ...meta, - image: meta?.image?.replace(/^Recipes\/images/, "/api/recipes/images"), - }, + meta, name, description, ingredients, diff --git a/routes/api/recipes/images/[image].ts b/routes/api/images/index.ts similarity index 78% rename from routes/api/recipes/images/[image].ts rename to routes/api/images/index.ts index 9b5bab4..c9b32cf 100644 --- a/routes/api/recipes/images/[image].ts +++ b/routes/api/images/index.ts @@ -5,7 +5,7 @@ import { MagickGeometry, } from "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts"; import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts"; -import * as cache from "@lib/cache.ts"; +import * as cache from "@lib/cache/image.ts"; await initializeImageMagick(); @@ -56,6 +56,11 @@ function modifyImage( } function parseParams(reqUrl: URL) { + const image = reqUrl.searchParams.get("image")?.replace(/^\//, ""); + if (image == null) { + return "Missing 'image' query parameter."; + } + const height = Number(reqUrl.searchParams.get("height")) || 0; const width = Number(reqUrl.searchParams.get("width")) || 0; if (height === 0 && width === 0) { @@ -69,50 +74,36 @@ function parseParams(reqUrl: URL) { return `Width and height cannot exceed ${maxDimension}.`; } return { + image, height, width, }; } -async function getImageResponse( - remoteImage: { buffer: Uint8Array; mediaType: string }, - params: { width: number; height: number }, -): Promise { - const modifiedImage = await modifyImage(remoteImage.buffer, { - ...params, - mode: "resize", - }); - - const response = new Response(modifiedImage, { - headers: { - "Content-Type": remoteImage.mediaType, - }, - }); - - return response; -} - export const handler = async ( _req: Request, _ctx: HandlerContext, ): Promise => { - const imageUrl = Deno.env.get("SILVERBULLET_SERVER") + "/Recipes/images/" + - _ctx.params.image; - const url = new URL(_req.url); const params = parseParams(url); - if (typeof params === "string") { return new Response(params, { status: 400 }); } - const imageId = `${imageUrl}.${params.width}.${params.height}`; - const cachedResponse = await cache.get(imageId); + const imageUrl = Deno.env.get("SILVERBULLET_SERVER") + "/" + params.image; + + const cachedResponse = await cache.getImage({ + url: imageUrl, + width: params.width, + height: params.height, + }); if (cachedResponse) { - const _r = await cachedResponse; - console.log({ cachedResponse, _r }); - return (await cachedResponse).clone(); + return new Response(cachedResponse.buffer.slice(), { + headers: { + "Content-Type": cachedResponse.mediaType, + }, + }); } const remoteImage = await getRemoteImage(imageUrl); @@ -120,9 +111,21 @@ export const handler = async ( return new Response(remoteImage, { status: 400 }); } - const response = getImageResponse(remoteImage, params); + const modifiedImage = await modifyImage(remoteImage.buffer, { + ...params, + mode: "resize", + }); - await cache.set(imageId, response); + cache.setImage(modifiedImage, { + url: imageUrl, + width: params.width, + height: params.height, + mediaType: remoteImage.mediaType, + }); - return response; + return new Response(modifiedImage, { + headers: { + "Content-Type": remoteImage.mediaType, + }, + }); }; diff --git a/routes/index.tsx b/routes/index.tsx index b002168..a8b7075 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -1,6 +1,8 @@ import { Head } from "$fresh/runtime.ts"; import { useSignal } from "@preact/signals"; import Counter from "@islands/Counter.tsx"; +import { MainLayout } from "@components/layouts/main.tsx"; +import { Card } from "@components/Card.tsx"; export default function Home() { const count = useSignal(3); @@ -9,23 +11,15 @@ export default function Home() { app -
-
- the fresh logo: a sliced lemon dripping with juice +
+ -

Welcome to fresh

-

- Try updating this message in the - ./routes/index.tsx file, and refresh. -

-
-
+ ); } diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index d09f759..88730bd 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -3,7 +3,7 @@ import { RecipeCard } from "@components/RecipeCard.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; import { Recipe } from "@lib/recipes.ts"; import { getRecipes } from "../api/recipes/index.ts"; - +import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx"; export const handler: Handlers = { async GET(_, ctx) { const recipes = await getRecipes(); @@ -14,6 +14,17 @@ export const handler: Handlers = { export default function Greet(props: PageProps) { return ( +
+ + + Back + + +

Recipes

+
{props.data?.map((doc) => { return ; diff --git a/static/global.css b/static/global.css index 7c270fa..67b393b 100644 --- a/static/global.css +++ b/static/global.css @@ -1,3 +1,17 @@ body { - background: #1F1F1F + background: #1F1F1F; + padding: 0px 20px; +} + +.noisy-gradient::after { + content: ""; + top: 0; + left: 0; + z-index:0; + position: absolute; + opacity: 0.7; + height: 100%; + width: 100%; + background: url(/grainy-gradient.png); + background-size: contain; } diff --git a/static/grainy-gradient.png b/static/grainy-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..b072539fe4cd7e50df220893d2af83724c3b28b6 GIT binary patch literal 31239 zcmeEtg-;yL^Y-20?oM%c4tIBVS|~1u6nA%bcXxL$?(Po7iWDjC(D(EGJKor68*)DW|HXrmUi*sjseWprNCu`_1U9iJ_UXmHBr|2Z*VYowbF{ z4@Y}1S9cdrH@{&25FgvHK#w5bh$!#G=-7;S=d9F#oV4V)$drVbr1aeI%+l=8lDzW5 zqN@Cwrn;8;iiY;?*2dz_{<7NUu7QG{q29`oj=s_H$)8ihGh?$8^Mln(({0PE>kAu; zo7*cpYrp39_7As?e(xTip8YvEzq~s6dwp|zcYpEl`1Jhp`u6_u`R^dse+&R{O_7rn z)AY#M?s_rQ)M?|?-`$=H1>Lg$*WG`S0sa_o{<-;* zuP4tKnw!8^k%E^H>EaKY*uF@9K=kw9yT7bHm^UK(|CHd~qmC;mg$~9qBw&$YFWu$u zCS%I>P=n0gw@6hlE5wKAQeP`60$DMHIY4{mgG5Z&aa^O#PHo4oU16s5sf*%h#c> z{Ro*V^d)vRD!<`N@gL|x8@Bk&dda^g1^=}NWxr%guluV(530!ib5ijSzj6@W_f5gF zom0bV5>Lip>MKthJ@LE!W&X)5V(Zc&^q7gJ9r%ybrGLS#h>iYV#8F>eI0dE}-TqIC z&A3y-WiwL!_}*}lIPkytXL?2Q>u@}g64>|s8++v<6*}92gZMFHlKBbJ8AZt?{7rrirUk{f;ZT>Qo7E$i7o)X`S zNkNB!F+91yCjmGym9u4k(-}nr?$c*~$6x2ZYI#K|&dY=N%hx#f+or{(b-a6-kk{ zj`^rYH2ilCeR04x|K-U5!V;&EG3RDW8@SnJGuA za!O^kcP{uQbu+}@iM{2r9T?$>DZfts`C6C|@6J%dh}QKD`RH0ci>p_1*=D79^WqKZ zxI;5j1i_vyt$$-ip?RAkHJzsrCJZ~_q>vrkL3?S%dMzNojtHjClCLlJ-HYFow+c_h z=$reeH!s0fD>g*~ioYf0mmfNV**4Hb20izNxfTZP zAuK8Iq0(5Fl#Vh6oCrGN#d4@i2zTMZBDiwX@P5NyWJjjDS&`mG;u#p?b*k|B(4Y1% zR&Zr(cI#plSR}pSC~$#&umy6VoQ2qK=cfi4AtD=LdjuK_-XOr zmqSgF>;KMSAq58Wr6_rii)qlI5{8v=ghc}_in2E2SttjKIR1tQ&M@)#xg>rZV*(vp zh8SsM%vQ;78(7)V8Bke7hV7RT)Z_+P!V&z@9U-E|7D%7fLzhD@P917+Z{79k?ole@&Aj#ws0t(oq$`%emT^M&eB1k_0mxF_8!79N#Gf- z5ngz44UJSic7*>8WGNZ4juualfgT~=Ofd3=tsq^?*8?KDv@mZUqK?(;WHkQUuj2Q@ zUl!g|b`)Yh&bqwrzuu65X5C2J41-b~r$|<@p9Yl!9h@eaM8Ym?)MAxwP9khW^_a3& z`Ug~R{VD=<@QuKXD5KrAnl(~e45bO7Q$Xpr_{vMd45%VlC~CUW2By*^w-$*(JXD=H zGxSXi7_x!r?ti|-+*f}w8IfRX;NlF=Yl4lfGKMS1n!V#nL`o8>^SMM~b~^&9HQ0V?tHLjM=5 z@EY=OSAs`q=OH@JG!fM_PG*ej`)Jg!tQXG%@Xp-d%(1oxUHKhckVZbonJL~IupZ5a zW^_FQIQL27cSfb5)?MIf+dct;@1Qw09)amG6d8t!gO#lOonmyaPC;y0X+(341#;Asc z-aaqS*OeaX+O{+(Jrmd<7P@%yFi^B8^Y|6D3FR(0Ua;qjE#HA9V`_iAS0a+FZS-1n zb1K~QTOj=U<32(V5zdT7IO8yv6J^IR_W)=sOfPoWuNb=nGC(Rpi#UK79--U06rp$4Dk*s0%PUNOT)@2d;{q%rFlQ8 z9IAInJN(X@Uad5xKu>W5_HK-}O-CgL)*9($`)l!pQ4rstHj&%~r2R&MBcbD}|Fey# zxNy1(QUur=;C}50-w}Vu-&shYTpNoe+Z~uGLd=JwOR2rpQ}yl8H*JZ659mE0{8*6I zY=p04$chU~NXkPI>F@bFy4yFX2@ppt4oA8-#@~IA@H2{w(+oZDUn%Y>WEbTKLJlTZ z48pk~D*mX*r&!0O+=7rxRTNEQym?3Tu4}D2AZ(zf<<5kCr+7suta^M4`T6VCHgy3R z$34S%;DW7Kn{8@F(ra(ZO_E^dl+dJF(2e8Pi8scH5`d{h5l$`8$cJIkOfY=KL8-rP z9TEZ$eA+V!jX}+jBfX=H%b>e6W^Afu(1oIDh9b}0sG>_HhG|CdYugz>wKRkA9R9Vn zAxs%s%iT;*)uv)zR`6-65o3p^8)2z*Kj7B+xIqt(y*gu;x^Wdm$EGnY-Myt)l%XZO z>`h1F)35Pc`{lweebPJ(#-%|u0fSt9K15WT=)V%sYiNw9n3&-!GMJCOij7L9HvLAi z0WDIvs_a_5Y&?M7Mxdx!Pk<7`lD=@s6{Z#T5_p;5BSJk5EVNkWb)!%+Lu@84Q#kz< zA867)OmJT6m3zU4xP^1hH53j-IXedhX&XaA%D@^zjkX$P>)*<6WTunjzHYwUrNlu! z!NC22Iu_;(H3#K~enj;=^oDgV<%j#|C&)%AgnysD5%~o`a*H(`ZYvojId}nyDd3;Z z8*DR4bi_~m$%i;p#MZ!9sp*8xHz|i2HhoOTp(ft6)MIE285mNgcA(Sc#J!}F$%>qb zj2i4m6&~M;$$TnpEUv)+Em5@p3h3aQB#4C!q5MW>CnMmtk>WcN zF@oJt=3T?c&GbOJ3%?7)o46qN&Jpg&#VO*Un7$Zjz(>0=y1+tYZ{|qDBMp8D*&~#2 zN0_-8J1{jJCy6nMBhO|>V+fc(#83~hAl-%x(puoE$7%KxA7b+#VwntJ6fDfp$zrKb z_eXb*vp9DQv8AJD0?iDO_k;LM*`_7p9YKNfC(zdMSfA0c10nd5CKgQGVb`=7^bR(5 zuzm+u#3N$~>`p#RVaR)^i$2F}A{n{tbYGIdNyL$%!3YFu;<~poFR2hOjUCQ5Z87kYHMnV9wh$A(fwu`$J0)?_;d?(R zGg70hA_5B;x;8pTR0)|7)iALgAN=S$9TKN%5xU4?p`@@Z+`JLo^n9H>oEjhku19uW zxdbFyLAQTlTo8klC_x@mqKRo%cdbz|PZY2?v;zflMF_PcroP%`4QE7;55}l5Qte8i zdn|AXh#VUg|E_z3PN~@yGTbyDU(DYLB{?E1t}kmFJAb~T6UV5=Esht5tva94QXked z7n88#p^Lym6^bRZ`OaII>)!Xp_P}o2Ep}4y?DV0N4y7q%H29Rr zmMTY*fj<4zd?WrLd_A@7(?;^vPP&n#vMH(_+rQ(1vPnXVdH9%!hX<;P*d$~9YJVmx z99qCEOn`VQuPhTZ7;#_%5gz;oy=*9YyauK>s8>#oSIv<^7au1M{zP1D^YFqA;tCzo z24)$Cr>Ey-qAB~LG9=dkKx0zdZxdbUJ8mC?IWY3SOx*veCVdbgACVIFLhoT>0*Gw< zYqQe_zwiiDAzM&x;irlc4ZZdW34fO3MPMKZ(Rb)((_b}07jddZ7lK2kN2Yd6WeCUCgZ+0N##8YC|*1oI!d7SMbIM_E-ogc^MKLc8TyOMG(rJbO+&5a6d55TsV5S^ zkV46VKp0Uj5XW{Z4K*P-1$``HW_1MvCf{yE3eh3LwiKiQqd7oPsf)4;rY0L>g-kH zjSapG`(l65%RwI32a!-9)d@O~xCgqJ^4>^QkGQEdC^Dz&05(`Z2rZ8{N$dLlfEq5A zs%{N_4j0=F{5?rY2PPJq#lbn2ehAof%L0CEco7#6CKIyzCFA`E2FS4Vz4<_a<*uv}^}Mb%`i zSYrdcy;;9FR!fP=aSm^wK3O9=hg>N|wMoY|I%k=DYVs^qun302DHNHhNfA3!AoSEI z71;b2c@D)r{HfIE=)Fq{_g?5;V;T`l0^ns39fa1U?HvV?6|=4K1YLQ$mOTlp;DaM(zZN!0m*qX7qrywIN7rNv zL4nuRj)~wq*X%O1xMjw21mQY@=#BotG4jj-Mq9FiXg?7j4_Y01hx$b2(Xu`+kOXbI zv!Z3cEz&*cuW{(Wkeu5nmixH=q!?@1%L11*)n?+LD-AxmvDsl+?zB7Rk7R5I{@~eu z1V3~yxFk6@*snnBew3zXTF@+~7+V4^z{D~M%a1Nabki7!j=l_gP*|UAtdc86K@YDY z4OPbh$R0$29n^tx0v>Dlgg8;;R`Bvf&INgxHgV8TWL~(0weP?~nX;kFs<;z=wUQ2^ zO|Y0$5qZ`l+(b30kBuNB;4GmI?MBZ)vvwH&yPY2V9BTzKD}qtBG^M)8Seh`R;?LE` zH&!mkDt64pU(2MNH8Ij>NgJ1yB%Kwd;Dv93<^$eaO5fut67pcL4k{r-$MG%v0<*`2 zVW3hmF~S`#_G2I!8$;b7b%zc@ZSd(6j*cTbB<|1)=R&<3xqv~+(rLVK61Qy9s1fqW zMx03;*@4vjjp(NN`I54t@0_%!lg36F$NRqoy$+5klj!$_k24Y)Q0peZtl~UOa^nbdKSdkC=BK1{6@o`puvZLqECd zo```*8M>H*#@*cq87{j~=NP+x)GrmK2UGl;a_1n@go-f&c>eT6kVKK4pQ=YcvK_r& zi%x)qP~PNkOpbq!VWeJz@!xP1PK>u5NOaL9CY(Obq@;{a94q`xw%#-6U33YcVA!^vd~ zLeNZ?o;fOHN`!eHLa*rPae>fT6QUUpAxR+q$KDW;?v~!q&c-L{lSOFFbaJHHj1CT?E^1*HnM0$*TqC{GQ-vtAQ7Tu_4Jj7q^<+fnZGK!YU#W1J zb_yEwF57csS$lRaLnDV#`3uh}G8YsLmJbH;Q9|v_d5Ve-gNFr zv64In#2y;S9pc^098Xiwpq?FMDiW(SXJ5V-DWrpEjyVO!o65HwyMhasymjy*NzTii zk(`E(+lgD6<`Yden=4$po6TVd!hNAeYjW-n&XZ|B0ScfnWSf_7AT9I>(J&Y(H*v5U zlnjA)l;_y9o9Fa}la<1eo9|Bfj9A9ow>Y3_M<6zFVnC;_FtjjkR5{e+tymvuu-#u2 zUtTJzDr69$3#Ml$`_zr*ggn)~e^c2Bhi$>QdDrE-f(`YJEO8nxIyUm6u>>5DHIMVL z;Sm8U4wxlSx`Cnzthi9w0i`pRjRn1(HKvkZkd&LQk=ONctq~Id>P|=j1h#MXHH_4> zmybapF`z!*F>*l=MButun|WO4XbXn+>#>7)gos2A>ug`qc90nQ+$zgg-?6;!;8EHFKYKtWZPQg1cw7f~VSu~VDg)4gmk4(|q zz+oTMlqWpXwDt9b6%x(b0EVei)MjABqRkvY+mBEQ#;^wUHb_#G=>KF0n>-jfUI_&xAk8vJe3*HAFHQ#DA!B`-iR zOj=wZAy-K@Q16UMjvVQG_UQo|o}V_ND(|`1nQI^!T6yAKQ=!CdK=$+`2padE6X{)! z(`jGo8Xeo95A{G*;RvT`ocAjLhuSjJ%1n-YAjUEfOYy7OhN~Pmf#Xp@G#}iNg#ycQ z=FC@g7}S2i=JjB7iI41Q4YKIO3+BdGW#C3lQjWP<0upadbI!X0@@;8Svsnuq!|Yc% zV6bzVv3BBuz~MGuT**bwWR5>ez9TnjUJ`z%8Ej@uSPTeE;Dah9M_{iK4QI+v_J3r``VH3PT=}}ka{}h=m!VORH?z@yxSOV%7ERAJ}y6R2>mX9OFv5g9H1hHkvXl(9_32RHUxrKTZ9~8#b_|#)FjmJ()D>3=O|JJ z*HNHCWoprmGr$Hr1~G{xYZ#H`@8y_OM6iNO86@N=676@*3M#AM&BuB5kd@Iz!>qA? z^-L62Mo~!-|98nuCFZ1AAnSy_rjk)FgzyKGZ z8PklMb(<0b)i5#=LQ!g+pRW%+ebkIxnxs|&B3)-5rB8|~6I8~{rOEG;rBRsU&^%f~ zUa*3e6IPNgQ&gj=iBwi#jY4&pg$iNNDCv9XY&~d-D&a>FJyJaLq{BMwIqkWM!7ju| zUNRe%vMNg|D@jZlTdd99KinL_<8HubWd@}$qPi-`vXKRG-r^TR9vu9D^rg& z!w{c$<~0e6s=)7xj=4*JI4SLi-ldaT8JSZcFez?J6VL^em2ka6Phk*#{D0EhV$zjeqptn@bo2FFG z)YI)tBI=@KLBd#fDK9WuXH`>5AY)k4R{Pw*cXZZxS|PY6$sR^QEm0S#5N<5ulnr&= zadis(#0b1`tO2JzeS{hqk1(QCU&e; zR#4DG&71w!3KO*>L*H#Is@CyLXlx27SC5QR0^&Hq_+>1mbQV?Ln&d^n1tBSp6kSut zvFHwIb-q^>u4_^;u=EkaV-wWDrdUo@PtH|&SYlAB(c=WMvLGG}Qq;iI1<|sqy5LYm zyklTHiO%*^Q><**jL>mUBiBhW5Y}hFaeHXsKqnw;B_jAlc>wdqT^=^I_WHj0Drx%E zRuSK%Pep@@iU1e%niT+UO1$stz>9C^$B2^v1u>l9_bvvMHqKV(*i-3hl_Y@B?4 zIV!ik>GHC>ca7PA32Y-q6<(M)4sJ=l2 z0wDiCjTZvHJx#J}D<%5)*iV8etc*E27U&e)q@)?9PS9G<-RnaDxPZNz;5g>RLhETodA1+SDXDb!6zs zKV=7jQgndA0hYCuNCMBvA|h8a%y}j=0vQ#}$>P6`!@%(|)q6cu}M=|HKeWLNx0 zBW~b&{Xn)7rHPuFpHT0rAJhFfQodFsoIj z@-g)d^<}9-mR>sxU+zKmw@W!|VXaD+D3$-Lb>FcxDyNe**>tO_ChOI6H*Xb|eP~t? zq_o8sr>nDUzG>FmqEqyani(_$vXUih`i1FQ*O-2P`;jW;lZ1%V!GNo?B9&&?EeQka zfJ`#e=4zq@EgJ+0(9qyCMygoXTUY(5Q?FEJpeu>0|4!MGlAdm84jy?S(^E7T2$YHW z6ZdJ3uH}u8RDLWG@H^`mlL4?Lhhtcz&b2nIyqLMD&VDyd;F!2DZl<1zXI2Wfu=0gj zqVM$?IPD>0sYTZ?M?o>M3|u;83bJMGU(&`%^$L_v%?;$wD!yl z+O#y;D$^O|GXdc#I2b+3j%Vf!cTKuJ(mosNUIfAw$`(>ep(su1fstOF_n>)#+$ME) zItC}gjoB%d03NWp%yT8)FhEU>yjwI$wKC|O+a`1PZ=c7)I5(o5raFu{ci>(Za@&YX zY6|@OntjSZuy8-&#wR8L^85Ar-*SVLloVN(*NvpjZ<+ZsWdJ;jxpp%GV z&GBNTBU^8~_9#T(DpPqT&6=W@quK6=D%Ymu-{Fx=itV-`Jz~^}73P?fY*UkUl(1F( z+B5|Q>Ca;Ieb51wpg_`RAki)iHFeV@_-kY8sC&3cMh`?Of-u;^_{2PM zAUp>2^^50IXP?fXSH8$Si%w`>s^2`WJk&|fE+X%URg=^_JFe|yj9yxufEX<;{=P&% zW%sL`qbR^1#NSEVi$l%3sm>Aqa_Z(Is1&HQgnQWGQ$Z6qX`a?R*Ncx$yn3Pgwfv(IGUb;aV_7cp) z0|;U7T$dp|Xjl=X@zikGu-N(;i#GDHw#rgcR_x_1Whs=Y5-{jMh`3sSbp<=R0Gtyq zPN}-5Qe_JWe5}vCtSLM9%E$H4$mIgD?MP4k&)VAB$`!V#KKs>5>SuAv5Uc~#ck2R) zKVgfAl`Um$m7UVx)b@GgqY;ixN|23A&W+P!?wjYKT>1>Zsr}(`;`J;3rAjG+L~6+= zs;6PR_k2dS954RI#?*$_uY|~2eC|X)?N!dj%2k0Y+}SXS#kT8S*g=OU?fHy)t#FCO zftNbYI=r|`a#L5S+}TLsV^O+u!I0v&Y-G;|^%Ke(pUWxxa`k-@CjZZx#!ZLZ*~;E{ zAH9-$%;3UlHQ00WMNE2+rqbA0^MY`w8s?|cmD zdX{AVZ)4d z#W#{Y*3Q{VJ17gxs&tHgz62|R%0JPMsO#I#4vIO4`fC1;=Ve9ulLyExgMtqO}K@O7a?YBoEj20Rq@*e%04J1b?v&&D4T43bz9X+tn3vd z@3H_B)^{=JO1FJv9aJ4;743Sbx@!FZuh2>2T-TOrFLN$4{^`!LQRG6wp}6 z*dqM1@}R_3Rvm@-U-oxeX?JWFBDAjC3>@)$V3=}Ab)6`wgJaN%Jxe;s zI;)a314l}(E#v5XjUr6mF;KJTx1!o$#CB6tsu<8J>^S}b@>hfLIjqXgV!(bt37EE6 zq0?p3Q1aDC?kXyjc3IRk1Xkg2Q3%v9T-0RIM0CM=l-VG55Y|OXrt8wiK32OOYS{-9 zmTbjREtunm8QWgCQ6rgZc`<%W90?(T!tvXLLTdGTP#(ah5i+j3>G-q3}RSF2whB|BxxPW z>kL97C7J{&I;$2z4>B;9b^m4C#B3Xnu;_c)6#mvd*;(KdEcTx!*n+P`Q`gZQ#xP0z zQ~Wk#vP=RNYiS$o3gNg|J6&@osZuLem(*%sE?vKwHJdWUYHLzpS}Ew(ygjv2XMr(l zOqmqCRl240EGp0EA^dicl9nxBTq%@xYQdMa{1~GaTbI`;aj83S>3Z!QVqofv_ii_{j)(Y|M$CGSkIA z_8$A~)bh)wmWtg|C=pa{$EE)IF@Gy!PiL94Wc{)kR<{e=TgzHSUR6)r{LAc`=ql~b z{&rxr!KV1NCjX9rXJI$5dFS#qr;xe*ksTjpK$u8US}bGb)l)P7l7=)%Jl6cj-rZYK z5#hn>GyY-O>T73P&fKX@!wo@Ox9@wlukSZM&(23;X_M_Ad;Yt`BkisaXq*s{f7_3p zt;E7DJU{a9*ae1GjaI;Hmpj6jO3aYgoB<1;UF7$I<**Hr_6-6bW_1gg@Q&_p)3fb< zv;Az+n$GA=!Lcn5Cvt@durD1%swz?UBuDj!I z{oEu_P+InQWBQA$x8~6?_EH(fqTXTcwQhq%#OeX#9o$>rnl^B~bCbwG@IJcUHrtja zEnM6NYcMHFBQRlLHRfn5PGYSA@>LPs{$pY)@-}Z&UTs7{9W23P+kbA z!EAaeAmHmmfc)J4kf51ePIz-?j!fR!Rok4%YIA0LqjhUf>4#>vv|e_S%qBhN;2kbe zLwYvfj@G^ws4WvNL6f$F_p3fRQeiy=U$4Y}FXLkLf=|&{acz%j*ii}?P7hvJ*)GZOV`T%ZxZmaP! zgTB%nM%NY+)DPF>*rTpiu(NkB;I#WS8e$gKD>2%E#{3;cG+{{AJ6Ca4c$0^0Vn>EFGBfk) zL{eWhhq19uT)FliKD8p*9VN_;c8*{HR(1|9J2+3SrQ4TS=dUiW-Te2ghOPmZ6^lIX ze8Xmn+vRIKEliEyT>tgvh0w7VJ)-!dPC3OCV9H$^@}@SqK|G~35k7Ce7hKY z4V`tIaa7!&>Uwix0`rdR>MYI1qlB%K+BK~s#9S);yP6` z=)Zv2DHEJmQLLO=+jt7TSRN2Ic5pXI>lhMk@C&m8ZMd9ruQ)oG>lSW?Z#TU>vX zejnC4<@OkiC95csJ&m8fzK*Z1j@8yWk!ULHBGzx75@H+t`Zk5Nwi-S15HAt0M@w=y z{J_6+IJR(F{%~_hs2Gtbf~2I_A}uYmwtDB$iiLG}SoZJI>EFLs>c_LA+MdIx$(YaTp#ZBp$X zKbbLekYzC|BE(qs2{yyFdiH#w;?ka_Jr!JFYPjS>#B*#RR?*zs5ZjovI|n&EJr_M9 zraQog)z%WO1m!_`GKvI;gp-6qG%--zLqXv__H@6n{F)q?xv+QqBW0?X5m_bNUY788 zj}0bCJZ&P;c+V)ey zvf<=L9pz~i0sz=PV$q1wKi)roJ|Typww7%6n}Jcje6e=r;s8ik0K&)RBMY-8G`s)& zVQ?g0A~S{!5tqx|Ql-{eM;0o7=K*74E$vrvuu*?` zS|(dlbyxX;PY~!?N<{|kH}MmdX34ewa5sV<-3XTI%N|-Bv}*x&Wr8L+l&Y+2inQ968lma9>o`kaNojyDn7j<=#5V29RdLc~ z{NKY-6eVlde90ZNia#1Wve%+ys<2|yVRctmS2Q%ulYnbXaythuPJ9q1_NFByBCyd| z`3JQ=wd+XGwSwQYlIV zZx+~Dlld(tG0-$4(<0o68tNpMRK>HUvrkcB>fo7`=h^(>&}4^d7&qgtfa0wzJTp!% z4UA;WIwoXtMmqs;*(ndy1o3Sn3FR2d=IH4{;P(Qnh9h|uf=9qSmR26m^MfheO2hP3 zB~hK79W+u}!Jz~dQPvRaCS7cxenh(50`4L2MJs2OT$DmKA(N6X@L5&&6a2E8`2*ETq%m z41HlnL9~)c7T<&$My#Z+Ld(SFy|GV)QV&g`PZc-(`kK)%@^rmWOfxlv5Zxjl_$l#D zmP4hbOCl+xDr-~3&z!a&60*Mzr*`RH!Zl9=z~B@IxDTgju^b$rx=CWw*wVfxN;T_q4agA*MnRFa!On*%L1S`fQnz{& zO}Q5XiH?c~W~3=c7YQ*~KW;P(E@sYG8VTc*oZ8y!^qU88{MEf09yEE&t=JFT*w}xp zm>toDlxQ1;5yPg#Zs8!dp!R{R{u%{QkX>GcwC@fmJ80<&Y!wp5uDUUR-7AzsI=LXvmTmLFj?SO;z-Xg+Tm9~lIbdVrd z~qrHE8bYrN23cggAD(+tTLA~;|_PeP&7fn^=>}HN&o8T{){O>bY z-8A|Ijc!~>6(A)Yom<^z0MAbAsut3=hQ@aqvD{HJ@Ji=7=XZ29<_>|hZGB5?*yNQ> zL2i-Kfl5s==jI8owawpUSzTo`JMLqnU86L#-P_+aOs^W*V=Zj0o&5Zqz@|>U3eG&{ zwoP9(YCF$ixh7^zXUgP($~d~T-JCO~c8uF(84gw6ek3WYR-CS7@>p8)T)(-skT{c* zU3m@GF6>~dDl4&3jvU>Q*(u?NYcSNa%W0CkyErwMzPszdMr7oKgm$kUPt@@zS^9YC zQ_gd~*k$@S)6CqvG|h1&{%isZm1kyic+v>A`(QE39bySr6%4HH-3%~&CByEo9qh2Q zLoKUjB&je+E=SlEI?OAJj4U}z<(px~3h<&SOD?1Japv&P$h@2J2aQE@?+Jp-nqxIQiXBTR00z3Lxj2vd(?O?)>mM3Qm9%4;I>rN5N6lJ=~!o`fVpF@SfOgY$l!qkG+yP09I{t(c$5_+L0O%-d5s33 zIK0uG#V}1pQz&kJ?S~^SnUV@=cEvISL{p=7=5hU`dgm$si`O@sTVQkfs-^(vQKP3< zKN*|Q zTMiJH)$)gz7R^fs*=pZw0H4dI?a+{Fo-BUM?55_oz33QXVeW2Df$kPgeu4jr=f{iJ zfbFZpE2bpcoU}0HPRs$~hVX1@3FNpJwXq?Btv^vHOEJ%+{61 zqi*>WkBR^2-A51CsBeAo$<6&Q<-`}q62th_;`)g4!1bQ(^mCVX;7~@|y!pUD5MZ>eX}j zYd0fDJc+B#SzPK$W~v>lV1S3EMUPi)n{u^(hCp){NQG+G;%9I7*1m5`)8gg4v*u>F z!3Rp5yT!<7l`%H3rd)GpNu-xmJ6@#^sT%oUlq zV;K&AUEfp4YSsOVgDxVf{jd?7vJ z#i^p=pV83w>~LEbg0!_)nejJRXXs#9l3UH<1GX+8@1b>|uR&t7`3n|O)WPVxv;(8T=x!98)f z!U5AKMh1|Z{|7t#oWt&jrPxn*K`*;)0X$;F*|P@;iG_jIRx9W`TXvMey;J=3iP8U@ ztyt~SWZK$6AaS#)C`u}iDYoW*&eYRO?sr{ zZ61DNg(Y1XEj}^-0x8xKRxs+|!^3A`;pfO_%g9JeA;i;0@t?`2iNA}6i_5>!yOy%k zyq3I-RkE6vN69fJVZ*;@+~a$Nsw_o3C@`i$LQY-syCT?>=_o&JYbZMkn7n+|r12)+ z^3f)a*x3jCbQO_USU|;!)Q0|fX>Gm2*gKo?$^)lmEhB|_BPSmV?4rBDk`Ke<-g))q zD5Vc4%w=37HTA`=o*E5(v|ayRURA60M^am*!(W0wB%Hb`EPdJGD{Ckntv(hgUeEy& zflFSmYk{p=G_;+Y1xCTyP!t;t3fzLcY#oB9*3KxbtbK&j1j@wYUw5YoNQm7~rp%$B z`05Q%Xxv7Tk@MDl_~_EnkVnUV*X`#74rk1^WensD1JmKF_yqTys8LZ-S`-Lj1ssk94sL_IvC zNUbWsZUGle0ET#U4B3l z{dwKj`z@DHAtN(stajFHma0R#pUItCktQbgtV6kTiVK%E9x5VdC9_h*hbx(bqUij< z;_)Sr)7jd>!rBRXd(>i7-;}a$q7oz5#&wj7yK)IQ2eo!oqEJpWkrM^ATuGlk8@hhP zr@@!(c`{&KIkhc!Kd@t%alK*1IMTrXn6z%Ml z+#+3)+43&bq43DuX>BikK3*TV0mC~>JK58^>wmO=MQ1~sy6fxYY)-5w1<^cj>~4q) zBIzORuzbn&gNwlMCzj34nTEY#(Q$Vl)gC?4h85%=Ww!PIS*krm;#O=(mLGGTcd^*2 zSX5YT11=^a!_(hsQ1rqbIjP>~_2T}L+Vsfe{cS8iAf88!$*(zU#0%b1d6I({q_Jxz z@^+lfWBo8x5ot;9<0rE`d}C=}L^hSe)A&UVt(Zjk&EoKx6QJ1Vd1qmI_@0fe>kU=0 zNAiZOT4riWyWCZANJO-O_RuAm0)oL|lVjo%mV|Q^?Z-!UWmE>e@ zPrpqjucq+E;#o<4`2r1(y8&K4?f;&m{Qx$hG7otvB=BA@ZW!MLs{TDgE)KTr`#x~{ zi*emo-v9f~eE8vTRMz8>2qqkXJ5DbSczL{>YX8+MMKRF3UcdBEF4=lp2q=zb4KR0i zad{fK`NU&2?A2vj4+YjRm)(y^mooKwbXUV3EmY*KFQhDdlI-9W__^FaM@zL5X8h`1 zm#QoIU0V8c=KgEDx0R=}Gq;teXG^MmhLY3VEX|OL%Hpm1?c#CeMyb;C_Et`7hfH#M znvA!!WpYyT($g+8^$))%yH%m=X?yu!dddc$cJ@}D6FXTw_jZ3Z;|s0aUY2jW&7%iQ z_K1=O5?Xw$t(-Sz+L1toSe3po`&)3R(;V{UBwB_D0kbtJ?A3ty8(Y|%=_bG0Ln9W^0z=wdSB;_#gv@h z6k((@f~2(VFxDI&{J%fCFzzfCnfA((laiL)wfEB?u3Wy8GY2A>P1i5Dy!lCax=a%q zcXB_kW?ec;RO<4w_*?HD)�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%0DYjUT-9mAE7!H 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 M07*qoM6N<$f~&L=p#T5? literal 0 HcmV?d00001