From 7e5e45b488bc32ec31740c3ed4d829a089b855ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 24 Sep 2019 20:50:54 +0200 Subject: [PATCH] Detect and load lfs files --- .../scm/repository/spi/GitBrowseCommand.java | 11 ++- .../scm/repository/spi/GitCatCommand.java | 92 +++++++++++++++--- .../spi/GitRepositoryServiceProvider.java | 16 +-- .../spi/GitRepositoryServiceResolver.java | 7 +- .../repository/spi/GitBrowseCommandTest.java | 2 +- .../scm/repository/spi/GitCatCommandTest.java | 35 ++++++- .../scm/repository/spi/scm-git-spi-test.zip | Bin 23641 -> 26072 bytes 7 files changed, 135 insertions(+), 28 deletions(-) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java index 2a254d96ce..1bc59f8e1b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBrowseCommand.java @@ -58,6 +58,7 @@ import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.SubRepository; import sonia.scm.util.Util; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -86,18 +87,20 @@ public class GitBrowseCommand extends AbstractGitCommand */ private static final Logger logger = LoggerFactory.getLogger(GitBrowseCommand.class); + private final LfsBlobStoreFactory lfsBlobStoreFactory; //~--- constructors --------------------------------------------------------- /** * Constructs ... - * - * @param context + * @param context * @param repository + * @param lfsBlobStoreFactory */ - public GitBrowseCommand(GitContext context, Repository repository) + public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context, repository); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } //~--- get methods ---------------------------------------------------------- @@ -375,7 +378,7 @@ public class GitBrowseCommand extends AbstractGitCommand Map subRepositories; try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { - new GitCatCommand(context, repository).getContent(repo, revision, + new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision, PATH_MODULES, baos); subRepositories = GitSubModuleParser.parse(baos.toString()); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java index 7477e0aee3..21f4b942df 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitCatCommand.java @@ -32,7 +32,10 @@ package sonia.scm.repository.spi; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lfs.LfsPointer; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -42,10 +45,14 @@ import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.util.LfsFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.GitUtil; +import sonia.scm.store.Blob; +import sonia.scm.util.IOUtil; import sonia.scm.util.Util; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.Closeable; import java.io.FilterInputStream; @@ -61,15 +68,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class); - public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository) { + private final LfsBlobStoreFactory lfsBlobStoreFactory; + + public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) { super(context, repository); + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } @Override public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException { logger.debug("try to read content for {}", request); - try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) { - closableObjectLoaderContainer.objectLoader.copyTo(output); + try (Loader closableObjectLoaderContainer = getLoader(request)) { + closableObjectLoaderContainer.copyTo(output); } } @@ -80,18 +90,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { } void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException { - try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) { - closableObjectLoaderContainer.objectLoader.copyTo(output); + try (Loader closableObjectLoaderContainer = getLoader(repo, revId, path)) { + closableObjectLoaderContainer.copyTo(output); } } - private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException { + private Loader getLoader(CatCommandRequest request) throws IOException { org.eclipse.jgit.lib.Repository repo = open(); ObjectId revId = getCommitOrDefault(repo, request.getRevision()); return getLoader(repo, revId, request.getPath()); } - private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException { + private Loader getLoader(Repository repo, ObjectId revId, String path) throws IOException { TreeWalk treeWalk = new TreeWalk(repo); treeWalk.setRecursive(Util.nonNull(path).contains("/")); @@ -116,21 +126,69 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { treeWalk.setFilter(PathFilter.create(path)); if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { + Attributes attributes = LfsFactory.getAttributesForPath(repo, path, entry); + + Attribute filter = attributes.get("filter"); + if (filter != null && "lfs".equals(filter.getValue())) { + return loadFromLfsStore(repo, treeWalk, revWalk); + } + ObjectId blobId = treeWalk.getObjectId(0); ObjectLoader loader = repo.open(blobId); - return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk); + return new GitObjectLoaderWrapper(loader, treeWalk, revWalk); } else { throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository)); } } - private static class ClosableObjectLoaderContainer implements Closeable { + private Loader loadFromLfsStore(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException { + ObjectId blobId = treeWalk.getObjectId(0); + LfsPointer lfsPointer; + try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) { + lfsPointer = LfsPointer.parseLfsPointer(is); + } + Blob blob = lfsBlobStoreFactory.getLfsBlobStore(repository).get(lfsPointer.getOid().getName()); + GitUtil.release(revWalk); + GitUtil.release(treeWalk); + return new BlobLoader(blob); + } + + private interface Loader extends Closeable { + void copyTo(OutputStream output) throws IOException; + + InputStream openStream() throws IOException; + } + + private static class BlobLoader implements Loader { + private final InputStream inputStream; + + private BlobLoader(Blob blob) throws IOException { + this.inputStream = blob.getInputStream(); + } + + @Override + public void copyTo(OutputStream output) throws IOException { + IOUtil.copy(inputStream, output); + } + + @Override + public InputStream openStream() { + return inputStream; + } + + @Override + public void close() throws IOException { + this.inputStream.close(); + } + } + + private static class GitObjectLoaderWrapper implements Loader { private final ObjectLoader objectLoader; private final TreeWalk treeWalk; private final RevWalk revWalk; - private ClosableObjectLoaderContainer(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { + private GitObjectLoaderWrapper(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) { this.objectLoader = objectLoader; this.treeWalk = treeWalk; this.revWalk = revWalk; @@ -141,14 +199,22 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand { GitUtil.release(revWalk); GitUtil.release(treeWalk); } + + public void copyTo(OutputStream output) throws IOException { + this.objectLoader.copyTo(output); + } + + public InputStream openStream() throws IOException { + return objectLoader.openStream(); + } } private static class InputStreamWrapper extends FilterInputStream { - private final ClosableObjectLoaderContainer container; + private final Loader container; - private InputStreamWrapper(ClosableObjectLoaderContainer container) throws IOException { - super(container.objectLoader.openStream()); + private InputStreamWrapper(Loader container) throws IOException { + super(container.openStream()); this.container = container; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 24dcff01d8..4c02a3a73c 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -39,6 +39,7 @@ import sonia.scm.repository.Feature; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; +import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.IOException; import java.util.EnumSet; @@ -76,9 +77,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider) { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { this.handler = handler; this.repository = repository; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } @@ -143,7 +145,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BrowseCommand getBrowseCommand() { - return new GitBrowseCommand(context, repository); + return new GitBrowseCommand(context, repository, lfsBlobStoreFactory); } /** @@ -155,7 +157,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public CatCommand getCatCommand() { - return new GitCatCommand(context, repository); + return new GitCatCommand(context, repository, lfsBlobStoreFactory); } /** @@ -281,11 +283,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- fields --------------------------------------------------------------- /** Field description */ - private GitContext context; + private final GitContext context; /** Field description */ - private GitRepositoryHandler handler; + private final GitRepositoryHandler handler; /** Field description */ - private Repository repository; + private final Repository repository; + + private final LfsBlobStoreFactory lfsBlobStoreFactory; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 0730ffc9cf..547c6b25f8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -39,6 +39,7 @@ import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; +import sonia.scm.web.lfs.LfsBlobStoreFactory; /** * @@ -49,11 +50,13 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { private final GitRepositoryHandler handler; private final GitRepositoryConfigStoreProvider storeProvider; + private final LfsBlobStoreFactory lfsBlobStoreFactory; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { this.handler = handler; this.storeProvider = storeProvider; + this.lfsBlobStoreFactory = lfsBlobStoreFactory; } @Override @@ -61,7 +64,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { GitRepositoryServiceProvider provider = null; if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider); + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory); } return provider; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java index 1feceba652..4b854f6209 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBrowseCommandTest.java @@ -171,6 +171,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase { } private GitBrowseCommand createCommand() { - return new GitBrowseCommand(createContext(), repository); + return new GitBrowseCommand(createContext(), repository, null); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java index 0418bc3e61..eea8bc0017 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitCatCommandTest.java @@ -39,12 +39,18 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import sonia.scm.NotFoundException; import sonia.scm.repository.GitRepositoryConfig; +import sonia.scm.store.Blob; +import sonia.scm.store.BlobStore; +import sonia.scm.web.lfs.LfsBlobStoreFactory; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link GitCatCommand}. @@ -136,7 +142,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { CatCommandRequest request = new CatCommandRequest(); request.setPath("b.txt"); - InputStream catResultStream = new GitCatCommand(createContext(), repository).getCatResultStream(request); + InputStream catResultStream = new GitCatCommand(createContext(), repository, null).getCatResultStream(request); assertEquals('b', catResultStream.read()); assertEquals('\n', catResultStream.read()); @@ -145,13 +151,38 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase { catResultStream.close(); } + @Test + public void testLfsStream() throws IOException { + LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + BlobStore blobStore = mock(BlobStore.class); + Blob blob = mock(Blob.class); + when(lfsBlobStoreFactory.getLfsBlobStore(repository)).thenReturn(blobStore); + when(blobStore.get("d2252bd9fde1bb2ae7531b432c48262c3cbe4df4376008986980de40a7c9cf8b")) + .thenReturn(blob); + when(blob.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[]{'i', 's'})); + + CatCommandRequest request = new CatCommandRequest(); + request.setRevision("lfs-test"); + request.setPath("lfs-image.png"); + + InputStream catResultStream = new GitCatCommand(createContext(), repository, lfsBlobStoreFactory) + .getCatResultStream(request); + + assertEquals('i', catResultStream.read()); + assertEquals('s', catResultStream.read()); + + assertEquals(-1, catResultStream.read()); + + catResultStream.close(); + } + private String execute(CatCommandRequest request) throws IOException { String content = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { - new GitCatCommand(createContext(), repository).getCatResult(request, + new GitCatCommand(createContext(), repository, null).getCatResult(request, baos); } finally diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-test.zip index 8f689e9664fc97179530c73aca4c969b1cfe962a..addda8609058d57eae3a0f91a0aa9e66b0a187d4 100644 GIT binary patch delta 4369 zcma)82~<G zMX*|1z$I<95^bxMDs@%_i^Io?<5H`(R;>zF?B~C{fC;wiFT=VacRKg(z0W=8T>6&M zewwmDNoDL@IjIacP~ZKW`2WEs>Y^`KPJ$7X%#xKc8sqWsv+G(B1gS(3gobdCsnZwC zjI!igrWH;|ABbP%;?w!S*}NQh+MJk9Ri->*N0 zExWVTT;|~+jtj)=wZ5y4RdP#jueqpLLl;zhm~-^Bs`%0>UC8F61E}No|6HQ-sH+>G zMHly^=azo^X}wQg?DX4re)`w(^d}R>m(LCSqbE;1uQKL*VfP8i1^SiGk8df<+n$hj zw(+*;d2SeEey4fpqZI4jcFoB6X&=q@JP+e<{O7vmt1}I=?sg^bxtLqGj{dw6TY_tz z90}U9Wp;<9&SQ0hdCM|k?Prz99EQxt^YZn^tF5Bvhn`SO$ru&h)iV7!NTOb=BeAbN zcr(Yr8C0NY)ZYjap-k#mjtzkA93HjdiHi3|=TZ|sEtAd9t{Pd*s%3A_Vn>nP%oh(=r!K}cSM z6->HZB7SwTK7PkLX8&=v^z9a6rK3p+SVaXh3M~tY#l!}UMk%w|l(KxeN~6G)Qj5i! zPkdQq8l_aJk!oZG7Mt4o%B-5e3q_IsB{D^xL?MyLrRKBAtwMuPAUKs0Au@<;rmV-G zw#}dL=u*J3Yn>ks-rOmCXTyQv^)mCWnomo$2fIyR{>8ZW(4`>8PX58F&O+lxk8~{~ zQ(NHCJ*0N><7fN%C0jOM>&6vdWYwNHf(#m3&T89U)ZTNo?&h4EN7kwY&wlV&9h>~$ zf)8e-Vl!J#z1;E1qi65_@#28@jh^^1TaKN%jDJF#-?nO@yx8wt+B4ekZLDnXmIKXC z=NWe;P2YRVOS?Mlq`h-Kii{YV@e}FjkM+Z0u51Y1rf~B2sN;eW9G+e#ZH1pR84#tQ z!G`D|)ZRfB?2ArycD4yw5E|nP5qP+3wg`95+M94T#WnnVR4R3&!)z1I0?S*cV7i6_ zOJ#0mYh`X`>2hBP5WAVp6Zg(Gh_65-Kf-01rF2_1S=qa+3HO7;$`7oWpv zNg>OmEYd$p@0Kr<6TabS?*2h+$k!BV)VLha&sWMVR;xxXx2XyW)hemPQlP-qvO^FDY~Jtoa+B z$?#Htj!$1oaO`OAJQEu%QXIF+B{G||z@o9r6l$fqK#E&6QmI^H$yX5q6j&vAzjydr z+*wTVI;v&-5Ih7K-Fje!PiC-*{UU&|qOdM$>6(5cnwhFh}i1(MRewG8E61dBJ+VXCJA7 z@Et$N`3we!9fVSXVaMx=k-KfR$$PaV0u8jv#{o^qfiG2VD=w;BzA*46ig?@5$q>Tl zgCW|xFNyRf9MbV^uE6L>;l#sck%CkoPOyDVh~a4Ng2-%QTxpZYthhy~(kO9-RW2=5 zYZNL)fmJHa$5j@UtWa*lrLP3}n?$gf^5Ht|3`{yiZ|@ZB@ewr?wzkvb*O}hI)(Xp( zmslAbA3Uw)$JGnN_l;I>?eTy3`1ae4ErIbrOe(4$U0F=C2_#;4#VvhAQAkMG)`r%6 zM!@Ia6`!kp&!AUsJvmc1L|C@bZ`kTgo_XW$aQ|V)XU1J)Fa72BCo9C~E~Er-R4cdq z_VAC#=kM9?`z~eELcHdc#thJfm8iSNyfjK)u)aAo@5{Nn2hM#sd&|Y0Ig=vVmzujg z_jH)0@P1ULQ?46P6Y0`?CwI(iuYw{A^2AhlC`fX?)S}2RND(B%t_T5Kj|uDJpG>i# zv0`UGBUbBtAe&=@$wVye)d9?LQsuG4Z5gT|1Yg z3JA3v&5uZB|1UW^j(F{n$ZT(>E6tQlLAjVS!B%BY9VfWo2uY*wd-W?4L8}d=k`A@a z5CElGF13`X8zC|=99pzo$|ZQ96@ibQi?T2f>kWh7r6S1mIkqA(_$F{6&OjrIB=(?2 zV$@M+K(Jw?+>x#cXsxDiYYVCSL@ZR#Y^zSPWNnQZVVLu$M4dIvjq~#gbh-fsN+*!-NDa?9j8(RY7o6 zFC@+$0MtY-x-1w%6UAg4$jbKidct!eB}*y?+_zqbOYa9)J;H5$ZwclOdl-3;ZY&pD zJ_3QF5DxgIbI7sJ#&QRjhW74;jyNWWa+yGnr@`GZT&!{=0^DF0@W%2I95?7+_Bjcs z5UkfMzsLXU4O~T_n*dJdK4f4sw}rVhA}cbtRk?qy#`@%?Afe0e^xyiXwVB%iB-PCZ zZ!|~%w@gONDnOu7s(=}09#P3~QFa6j%ratAA`obfR>0aU9&pmQ=;a6q%`l?Dk+3jh zA!-%EuS9Q$5MnYjV2zoJUKPO?W+OTx3jQ=_KuR(f-5LeyMk9JF3TllRgtyb_Uj3x= zlR#6V9BvWDvn4=HH)2~Q2+UI|U{yNLMOK^CMHX3}xM3m5>R+0;ljSe#xAjd6mA(U- zQgC#y3|^)f(HAlpH_i$4Z8_vlG-BV$5oppVKsKHSLsPh@S^-DK8PTN*7?zqrj(n{I zMT!yYRw5vYRKWfe9vm=m(Yq?>Fc>kB8iC+&1r#KWMt7;9Cdr6BC5C<`h9Z)klpoVT zbea+M#9?h(2D~(K(Ze`!Oh$CjD41=^a8a~rlnWFQ8?DyDmDGi3SUk*1HlnlQp)olF4B1>XD*>iwr=lMxz`ksa>&Cu1#62as z3_4=;rW`IhQ3v~SGO;r{1TM$1$f#t}(;z;Fi;dAE@F;dDEXv_o9C4z5s&j&KMy37w eey98;Aib9KOvxSt964kn@mC&%Ad)eT-v0oNcim?I delta 2334 zcmY*Z3s6+o89saNU6ggXU6#mWSue=T-Q~^hvOsu0%6l)0u*>_gtUzeMf~JlENm|iK zJ66+gMiVQ>b|j9m8OLl)n?_8SOj1juZIeoDh=Xy))F#E6v}w}R#GbtiEY8l{z4t%= z`Of!!|9}2#e^C7OH;O}6{?8X~wmh9($xpw={`WmAe-#<=9JvkPVa& z715L4{v87;t}a)fyJ2e};f}Js-u*jwts@HlHb;p3IREtT4{+&f{(i_?d!v86|I+qd z#;4x;crcqAyO;XV*H0WdK55J0rfxL<_x_Fcn`vLx9l5)?{{^FN z;NUY&ou^8=-yNH6x%+CSc6c#l^BZRlUCr2D^?cpS6Y-y%+wryatMI)|=YzhvSUPao z{mI&=7B*a(pLurEw-l5wo4luN-CgSWQrV$^u13I#&{@QCF*1gylIv2e z2Ej8?e@Cn_6z}s|ycy~RSHWo1ASgC$`I~pK3ZN`Do{wN2t>k@bSboGKUX?uS<;^92-DgrHEl7KwRw{sUHf zjOD$K&od2}u=&{7Y%eS`!MC<|aXLkh+G6!e?6G3IVjj~*9TtlBqO3`aCz4e7(5hZy zs={Fw3(L$^OfO|=uqSf|Y>eA{hMLa5w!(oo+xdAfhw+pM44H#h(Aq3&dfA`1&tm6K zd-JYYZY$1X#u_PPZ1btQCCo{y5T}al{AC|v7=+>iAuH3D7ryX-`a3C0h=r-w;cnGp zAv=^F^}F*53&rJ(GXo7P!Uv77;C8Zse(ooh%wU#|GvQU69?pEfQF9$yAm3+09R8hO zf?Fm%{*k4|+i_m21w>@O%u_sMN_U`?uX7&9xtX&KXv78!cEl zGy;Ds2ws9v;nRoxHSl6owF^kdeS?wKim+sTa$npmU&i->(zNUUZtbMVfbxTC2myeXtxgcD$CeF z&~Y=71iuK4L{4=KLMp_-Nj)L`gFWtDu-C?+y+Wi^#GZWGu0<1RrMS&k07xA~T`y>mo{T6$=R519}R zSuFbX0~`9>T|Fc0swfE;;jLa6V-tXDb}>*9y?k7uI~41sD1_TZZp%i3BUV*Zk8x#z@Fxvbi#lynk8=5KoFc|!Q(A4 zIMprEV@CY0TcRHs5$aajyfNc#T{f-r54HDgI#gzt!tZaWK9_^Iq zJDIrH>C36GVLkKmfDPN)9rA%4S-9$uxRY4~*UT2QIAdh^=xnIANSr&HATQB^Lt6@k z(>Z?q)@&$Rj962z{9wHkV-3WWgn- zwYXBGiFxR*tffctaIP|5ZucRVNayl!&}FB(d@Q)Km@vI&vFNAygvdQEx2)VKqFbcz xEN5v=w^4{tvmNBvt?b