From fdc9bdfe840293b6dcae225563b97dd708c8d219 Mon Sep 17 00:00:00 2001 From: Gethin James Date: Mon, 6 Feb 2012 11:11:42 +0000 Subject: [PATCH] FIXED ALF-12541: AMP files need to be able to be pinned to specific "edition(s)" of Alfresco It is now possible to specify a module.editions property (eg. community) which is checked by the MMT. Also, the version is checked on install. Also, started refactoring some of the code for better reuse. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@33668 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../repo/module/ModuleDetailsImpl.java | 30 ++++ .../repo/module/ModuleDetailsImplTest.java | 1 + .../repo/module/tool/InstalledFiles.java | 2 +- .../repo/module/tool/ModuleDetailsHelper.java | 2 +- .../module/tool/ModuleManagementTool.java | 56 ++---- .../alfresco/repo/module/tool/WarHelper.java | 48 +++++ .../repo/module/tool/WarHelperImpl.java | 170 ++++++++++++++++++ .../repo/module/tool/WarHelperImplTest.java | 134 ++++++++++++++ .../service/cmr/module/ModuleDetails.java | 10 ++ source/test-resources/module/test.war | Bin 8603 -> 9022 bytes 10 files changed, 406 insertions(+), 47 deletions(-) create mode 100644 source/java/org/alfresco/repo/module/tool/WarHelper.java create mode 100644 source/java/org/alfresco/repo/module/tool/WarHelperImpl.java create mode 100644 source/java/org/alfresco/repo/module/tool/WarHelperImplTest.java diff --git a/source/java/org/alfresco/repo/module/ModuleDetailsImpl.java b/source/java/org/alfresco/repo/module/ModuleDetailsImpl.java index fcf7576d7b..f4a31cda1f 100644 --- a/source/java/org/alfresco/repo/module/ModuleDetailsImpl.java +++ b/source/java/org/alfresco/repo/module/ModuleDetailsImpl.java @@ -52,6 +52,7 @@ public class ModuleDetailsImpl implements ModuleDetails private VersionNumber version; private String title; private String description; + private List editions; private VersionNumber repoVersionMin; private VersionNumber repoVersionMax; private List dependencies; @@ -153,6 +154,9 @@ public class ModuleDetailsImpl implements ModuleDetails } // DEPENDENCIES this.dependencies = extractDependencies(trimmedProperties); + + this.editions = extractEditions(trimmedProperties); + // INSTALL DATE if (trimmedProperties.getProperty(PROP_INSTALL_DATE) != null) { @@ -215,6 +219,22 @@ public class ModuleDetailsImpl implements ModuleDetails this.description = description; } + private static List extractEditions(Properties trimmedProperties) + { + List specifiedEditions = null; + String editions = trimmedProperties.getProperty(PROP_EDITIONS); + if (editions != null) + { + specifiedEditions = new ArrayList(); + StringTokenizer st = new StringTokenizer(editions, ","); + while (st.hasMoreTokens()) + { + specifiedEditions.add(st.nextToken()); + } + } + return specifiedEditions; + } + private static List extractDependencies(Properties properties) { int prefixLength = PROP_DEPENDS_PREFIX.length(); @@ -374,6 +394,16 @@ public class ModuleDetailsImpl implements ModuleDetails this.installState = installState; } + public List getEditions() + { + return editions; + } + + public void setEditions(List editions) + { + this.editions = editions; + } + /** * @author Derek Hulley */ diff --git a/source/java/org/alfresco/repo/module/ModuleDetailsImplTest.java b/source/java/org/alfresco/repo/module/ModuleDetailsImplTest.java index db02095420..af663fdd71 100644 --- a/source/java/org/alfresco/repo/module/ModuleDetailsImplTest.java +++ b/source/java/org/alfresco/repo/module/ModuleDetailsImplTest.java @@ -47,6 +47,7 @@ public class ModuleDetailsImplTest extends TestCase defaultProperties.setProperty(ModuleDetails.PROP_TITLE, "Test"); defaultProperties.setProperty(ModuleDetails.PROP_DESCRIPTION, "Test description"); defaultProperties.setProperty(ModuleDetails.PROP_VERSION, "1.0.0"); + defaultProperties.setProperty(ModuleDetails.PROP_EDITIONS, "Community"); defaultProperties.setProperty(ModuleDetails.PROP_REPO_VERSION_MIN, new VersionNumber("1.2").toString()); defaultProperties.setProperty(ModuleDetails.PROP_REPO_VERSION_MAX, new VersionNumber("1.4.3").toString()); defaultProperties.setProperty(ModuleDetails.PROP_DEPENDS_PREFIX + "a", "1.2.3"); diff --git a/source/java/org/alfresco/repo/module/tool/InstalledFiles.java b/source/java/org/alfresco/repo/module/tool/InstalledFiles.java index e53916d9c1..f0a291a4e3 100644 --- a/source/java/org/alfresco/repo/module/tool/InstalledFiles.java +++ b/source/java/org/alfresco/repo/module/tool/InstalledFiles.java @@ -176,7 +176,7 @@ public class InstalledFiles */ public String getFilePathInWar() { - return ModuleManagementTool.MODULE_DIR + "/" + this.moduleId + "/modifications.install"; + return WarHelper.MODULE_NAMESPACE_DIR + "/" + this.moduleId + "/modifications.install"; } /** diff --git a/source/java/org/alfresco/repo/module/tool/ModuleDetailsHelper.java b/source/java/org/alfresco/repo/module/tool/ModuleDetailsHelper.java index 26c61c3644..3a8f5f6b14 100644 --- a/source/java/org/alfresco/repo/module/tool/ModuleDetailsHelper.java +++ b/source/java/org/alfresco/repo/module/tool/ModuleDetailsHelper.java @@ -127,7 +127,7 @@ public class ModuleDetailsHelper */ public static String getModulePropertiesFilePathInWar(String moduleId) { - return ModuleManagementTool.MODULE_DIR + "/" + moduleId + "/" + "module.properties"; + return WarHelper.MODULE_NAMESPACE_DIR + "/" + moduleId + WarHelper.MODULE_CONFIG_IN_WAR; } /** diff --git a/source/java/org/alfresco/repo/module/tool/ModuleManagementTool.java b/source/java/org/alfresco/repo/module/tool/ModuleManagementTool.java index 905a3f8130..e44b089f3a 100644 --- a/source/java/org/alfresco/repo/module/tool/ModuleManagementTool.java +++ b/source/java/org/alfresco/repo/module/tool/ModuleManagementTool.java @@ -24,14 +24,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Map; import java.util.Properties; import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.service.cmr.module.ModuleDependency; import org.alfresco.service.cmr.module.ModuleDetails; import org.alfresco.service.cmr.module.ModuleInstallState; import org.alfresco.util.VersionNumber; @@ -68,8 +65,7 @@ public class ModuleManagementTool private static final String PROP_INHERIT_DEFAULT = "include.default"; /** Standard directories found in the alfresco war */ - public static final String MODULE_DIR = "/WEB-INF/classes/alfresco/module"; - public static final String BACKUP_DIR = MODULE_DIR + "/backup"; + public static final String BACKUP_DIR = WarHelper.MODULE_NAMESPACE_DIR+ "/backup"; /** Operations and options supperted via the command line interface to this class */ private static final String OP_INSTALL = "install"; @@ -92,6 +88,8 @@ public class ModuleManagementTool /** Indicates the current verbose setting */ private boolean verbose = false; + WarHelper warHelper = new WarHelperImpl(); + /** * Constructor */ @@ -201,11 +199,11 @@ public class ModuleManagementTool try { outputMessage("Installing AMP '" + ampFileLocation + "' into WAR '" + warFileLocation + "'"); - + java.io.File theWar = new File(warFileLocation, DETECTOR_AMP_AND_WAR); if (preview == false) { // Make sure the module and backup directory exisits in the WAR file - File moduleDir = new File(warFileLocation + MODULE_DIR, DETECTOR_AMP_AND_WAR); + File moduleDir = new File(warFileLocation + WarHelper.MODULE_NAMESPACE_DIR, DETECTOR_AMP_AND_WAR); if (moduleDir.exists() == false) { moduleDir.mkdir(); @@ -242,45 +240,13 @@ public class ModuleManagementTool String installingId = installingModuleDetails.getId(); VersionNumber installingVersion = installingModuleDetails.getVersion(); - // Check that the target war has the necessary dependencies for this install - List installingModuleDependencies = installingModuleDetails.getDependencies(); - List missingDependencies = new ArrayList(0); - for (ModuleDependency dependency : installingModuleDependencies) - { - String dependencyId = dependency.getDependencyId(); - ModuleDetails dependencyModuleDetails = ModuleDetailsHelper.createModuleDetailsFromWarAndId(warFileLocation, dependencyId); - // Check the dependency. The API specifies that a null returns false, so no null check is required - if (!dependency.isValidDependency(dependencyModuleDetails)) - { - missingDependencies.add(dependency); - continue; - } - } - if (missingDependencies.size() > 0) - { - throw new ModuleManagementToolException("The following modules must first be installed: " + missingDependencies); - } + //A series of checks + warHelper.checkCompatibleVersion(theWar, installingModuleDetails); + warHelper.checkCompatibleEdition(theWar, installingModuleDetails); + warHelper.checkModuleDependencies(theWar, installingModuleDetails); // Try to find an installed module by the ID - ModuleDetails installedModuleDetails = ModuleDetailsHelper.createModuleDetailsFromWarAndId(warFileLocation, installingId); - if (installedModuleDetails == null) - { - // It might be there as one of the aliases - List installingAliases = installingModuleDetails.getAliases(); - for (String installingAlias : installingAliases) - { - ModuleDetails installedAliasModuleDetails = ModuleDetailsHelper.createModuleDetailsFromWarAndId(warFileLocation, installingAlias); - if (installedAliasModuleDetails == null) - { - // There is nothing by that alias - continue; - } - // We found an alias and will treat it as the same module - installedModuleDetails = installedAliasModuleDetails; - outputMessage("Module '" + installingAlias + "' is installed and is an alias of '" + installingId + "'"); - break; - } - } + ModuleDetails installedModuleDetails = warHelper.getModuleDetailsOrAlias(theWar, installingModuleDetails); // Now clean up the old instance if (installedModuleDetails != null) @@ -639,7 +605,7 @@ public class ModuleManagementTool this.verbose = true; try { - File moduleDir = new File(warLocation + MODULE_DIR, DETECTOR_AMP_AND_WAR); + File moduleDir = new File(warLocation + WarHelper.MODULE_NAMESPACE_DIR, DETECTOR_AMP_AND_WAR); if (moduleDir.exists() == false) { outputMessage("No modules are installed in this WAR file"); diff --git a/source/java/org/alfresco/repo/module/tool/WarHelper.java b/source/java/org/alfresco/repo/module/tool/WarHelper.java new file mode 100644 index 0000000000..79f8c5b50a --- /dev/null +++ b/source/java/org/alfresco/repo/module/tool/WarHelper.java @@ -0,0 +1,48 @@ +package org.alfresco.repo.module.tool; + +import java.io.File; + +import org.alfresco.service.cmr.module.ModuleDetails; + +/** + * Performs various actions on a war file or exploded war directory + * + * @author Gethin James + */ +public interface WarHelper +{ + public static final String MODULE_NAMESPACE_DIR = "/WEB-INF/classes/alfresco/module"; + public static final String MODULE_CONFIG_IN_WAR = "/module.properties"; + + /** + * Gets the module details or an available alias + * @param war a valid war file or exploded directory from a war + * @param installingModuleDetails + * @return ModuleDetails + */ + public ModuleDetails getModuleDetailsOrAlias(File war, ModuleDetails installingModuleDetails); + + /** + * Checks the dependencies of this module + * @param war + * @param installingModuleDetails + */ + public void checkModuleDependencies(File war, ModuleDetails installingModuleDetails); + + /** + * Checks to see if the module is compatible with the version of Alfresco. + * + * @param war a valid war file or exploded directory from a war + */ + public void checkCompatibleVersion(File war, ModuleDetails installingModuleDetails); + + /** + * This checks to see if the module that is being installed is compatible with the war. + * If not module edition is specfied then it will just return. However, if an edition is specified and it doesn't match + * then an error is thrown. + * @param war a valid war file or exploded directory from a war + * @param installingModuleDetails + */ + public void checkCompatibleEdition(File war, ModuleDetails installingModuleDetails); + +} diff --git a/source/java/org/alfresco/repo/module/tool/WarHelperImpl.java b/source/java/org/alfresco/repo/module/tool/WarHelperImpl.java new file mode 100644 index 0000000000..889caf36fe --- /dev/null +++ b/source/java/org/alfresco/repo/module/tool/WarHelperImpl.java @@ -0,0 +1,170 @@ +package org.alfresco.repo.module.tool; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.alfresco.repo.module.ModuleDetailsImpl; +import org.alfresco.service.cmr.module.ModuleDependency; +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.util.VersionNumber; + +import de.schlichtherle.io.FileInputStream; + +/** + * Performs logic for the Module Management Tool. + * + * @author Gethin James + */ +public class WarHelperImpl implements WarHelper +{ + + public static final String VERSION_PROPERTIES = "/WEB-INF/classes/alfresco/version.properties"; + + + @Override + public void checkCompatibleVersion(File war, ModuleDetails installingModuleDetails) + { + //Version check + File propsFile = getFile(war, VERSION_PROPERTIES); + Properties warVers = loadProperties(propsFile); + VersionNumber warVersion = new VersionNumber(warVers.getProperty("version.major")+"."+warVers.getProperty("version.revision")+"."+warVers.getProperty("version.minor")); + if(warVersion.compareTo(installingModuleDetails.getRepoVersionMin())==-1) { + throw new ModuleManagementToolException("The module ("+installingModuleDetails.getTitle()+") must be installed on a repo version greater than "+installingModuleDetails.getRepoVersionMin()); + } + if(warVersion.compareTo(installingModuleDetails.getRepoVersionMax())==1) { + throw new ModuleManagementToolException("The module ("+installingModuleDetails.getTitle()+") cannot be installed on a repo version greater than "+installingModuleDetails.getRepoVersionMax()); + } + } + + @Override + public void checkCompatibleEdition(File war, ModuleDetails installingModuleDetails) + { + + List installableEditions = installingModuleDetails.getEditions(); + + if (installableEditions != null && installableEditions.size() > 0) { + + File propsFile = getFile(war, VERSION_PROPERTIES); + Properties warVers = loadProperties(propsFile); + String warEdition = warVers.getProperty("version.edition"); + + for (String edition : installableEditions) + { + if (warEdition.equalsIgnoreCase(edition)) + { + return; //successful match. + } + } + throw new ModuleManagementToolException("The module ("+installingModuleDetails.getTitle() + +") can only be installed in one of the following editions"+installableEditions); + } + } + + @Override + public void checkModuleDependencies(File war, ModuleDetails installingModuleDetails) + { + // Check that the target war has the necessary dependencies for this install + List installingModuleDependencies = installingModuleDetails.getDependencies(); + List missingDependencies = new ArrayList(0); + for (ModuleDependency dependency : installingModuleDependencies) + { + String dependencyId = dependency.getDependencyId(); + ModuleDetails dependencyModuleDetails = getModuleDetails(war, dependencyId); + // Check the dependency. The API specifies that a null returns false, so no null check is required + if (!dependency.isValidDependency(dependencyModuleDetails)) + { + missingDependencies.add(dependency); + continue; + } + } + if (missingDependencies.size() > 0) + { + throw new ModuleManagementToolException("The following modules must first be installed: " + missingDependencies); + } + } + + @Override + public ModuleDetails getModuleDetailsOrAlias(File war, ModuleDetails installingModuleDetails) + { + ModuleDetails installedModuleDetails = getModuleDetails(war, installingModuleDetails.getId()); + if (installedModuleDetails == null) + { + // It might be there as one of the aliases + List installingAliases = installingModuleDetails.getAliases(); + for (String installingAlias : installingAliases) + { + ModuleDetails installedAliasModuleDetails = getModuleDetails(war, installingAlias); + if (installedAliasModuleDetails == null) + { + // There is nothing by that alias + continue; + } + // We found an alias and will treat it as the same module + installedModuleDetails = installedAliasModuleDetails; + //outputMessage("Module '" + installingAlias + "' is installed and is an alias of '" + installingModuleDetails + "'", false); + break; + } + } + return installedModuleDetails; + } + + /** + * Gets the module details for the specified module from the war. + * @param war a valid war file or exploded directory from a war + * @param moduleId + * @return + */ + protected ModuleDetails getModuleDetails(File war, String moduleId) + { + ModuleDetails moduleDets = null; + File theFile = getModuleDetailsFile(war, moduleId); + if (theFile != null && theFile.exists()) + { + moduleDets = new ModuleDetailsImpl(loadProperties(theFile)); + } + return moduleDets; + } + + /** + * Reads a .properites file from the war and returns it as a Properties object + * @param propertiesPath Path to the properties file (including .properties) + * @return Properties object or null + */ + private Properties loadProperties(File propertiesFile) + { + Properties result = null; + try + { + if (propertiesFile.exists()) + { + InputStream is = new FileInputStream(propertiesFile); + result = new Properties(); + result.load(is); + } + } + catch (IOException exception) + { + throw new ModuleManagementToolException("Unable to load properties from the war file; "+propertiesFile.getPath(), exception); + } + return result; + + } + + private File getModuleDetailsFile(File war, String moduleId) + { + return getFile(war,MODULE_NAMESPACE_DIR+ "/" + moduleId+MODULE_CONFIG_IN_WAR); + } + + private File getFile(File war, String pathToFileIncludingExtension) + { + return new de.schlichtherle.io.File(war,pathToFileIncludingExtension, ModuleManagementTool.DETECTOR_AMP_AND_WAR); + } + + + + +} diff --git a/source/java/org/alfresco/repo/module/tool/WarHelperImplTest.java b/source/java/org/alfresco/repo/module/tool/WarHelperImplTest.java new file mode 100644 index 0000000000..0ded4ca9b9 --- /dev/null +++ b/source/java/org/alfresco/repo/module/tool/WarHelperImplTest.java @@ -0,0 +1,134 @@ +package org.alfresco.repo.module.tool; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +import org.alfresco.repo.module.ModuleDetailsImpl; +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.util.TempFileProvider; +import org.alfresco.util.VersionNumber; +import org.junit.Test; +import org.springframework.util.FileCopyUtils; + +import de.schlichtherle.io.FileOutputStream; + +/** + * Tests the war helper. + * + * @author Gethin James + */ +public class WarHelperImplTest extends WarHelperImpl +{ + + private WarHelper warhelper = new WarHelperImpl(); + +// @Before +// public void setUp() throws Exception +// { +// } + + @Test + public void testCheckCompatibleVersion() + { + File theWar = getFile(".war", "module/test.war"); //Version 4.0.1 + + ModuleDetails installingModuleDetails = new ModuleDetailsImpl("test_it", new VersionNumber("9999"), "Test Mod", "Testing module"); + installingModuleDetails.setRepoVersionMin(new VersionNumber("10.1")); + try + { + this.warhelper.checkCompatibleVersion(theWar, installingModuleDetails); + fail(); //should never get here + } + catch (ModuleManagementToolException exception) + { + assertTrue(exception.getMessage().endsWith("must be installed on a repo version greater than 10.1")); + } + + installingModuleDetails.setRepoVersionMin(new VersionNumber("1.1")); + this.warhelper.checkCompatibleVersion(theWar, installingModuleDetails); //does not throw exception + + installingModuleDetails.setRepoVersionMax(new VersionNumber("3.0")); + try + { + this.warhelper.checkCompatibleVersion(theWar, installingModuleDetails); + fail(); //should never get here + } + catch (ModuleManagementToolException exception) + { + assertTrue(exception.getMessage().endsWith("cannot be installed on a repo version greater than 3.0")); + } + + installingModuleDetails.setRepoVersionMax(new VersionNumber("99")); + this.warhelper.checkCompatibleVersion(theWar, installingModuleDetails); //does not throw exception + + installingModuleDetails.setRepoVersionMin(new VersionNumber("4.0.1")); //current war version + installingModuleDetails.setRepoVersionMax(new VersionNumber("4.0.1")); //current war version + this.warhelper.checkCompatibleVersion(theWar, installingModuleDetails); //does not throw exception + } + + @Test + public void testCheckCompatibleEdition() + { + Properties props = new Properties(); + props.setProperty(ModuleDetails.PROP_ID, "TestComp"); + props.setProperty(ModuleDetails.PROP_VERSION, "9999"); + props.setProperty(ModuleDetails.PROP_TITLE, "Test for Compatiblity"); + props.setProperty(ModuleDetails.PROP_DESCRIPTION, "Test for Compatible Editions"); + ModuleDetails installingModuleDetails = new ModuleDetailsImpl(props); + File theWar = getFile(".war", "module/test.war"); //Community Edition + + //Test for no edition specified + this.warhelper.checkCompatibleEdition(theWar, installingModuleDetails); //does not throw exception + + //Test for invalid edition + props.setProperty(ModuleDetails.PROP_EDITIONS, "CommuniT"); + installingModuleDetails = new ModuleDetailsImpl(props); + + try + { + this.warhelper.checkCompatibleEdition(theWar, installingModuleDetails); + fail(); //should never get here + } + catch (ModuleManagementToolException exception) + { + assertTrue(exception.getMessage().endsWith("can only be installed in one of the following editions[CommuniT]")); + } + + props.setProperty(ModuleDetails.PROP_EDITIONS, ("CoMMunity")); //should ignore case + installingModuleDetails = new ModuleDetailsImpl(props); + this.warhelper.checkCompatibleEdition(theWar, installingModuleDetails); //does not throw exception + + props.setProperty(ModuleDetails.PROP_EDITIONS, ("enterprise,community,bob")); //should ignore case + installingModuleDetails = new ModuleDetailsImpl(props); + this.warhelper.checkCompatibleEdition(theWar, installingModuleDetails); //does not throw exception + + props.setProperty(ModuleDetails.PROP_EDITIONS, ("enterprise,Community")); //should ignore case + installingModuleDetails = new ModuleDetailsImpl(props); + this.warhelper.checkCompatibleVersion(theWar, installingModuleDetails); //does not throw exception + } + + private File getFile(String extension, String location) + { + File file = TempFileProvider.createTempFile("moduleManagementToolTest-", extension); + InputStream is = this.getClass().getClassLoader().getResourceAsStream(location); + assertNotNull(is); + OutputStream os; + try + { + os = new FileOutputStream(file); + FileCopyUtils.copy(is, os); + } + catch (IOException error) + { + error.printStackTrace(); + } + return file; +} +} diff --git a/source/java/org/alfresco/service/cmr/module/ModuleDetails.java b/source/java/org/alfresco/service/cmr/module/ModuleDetails.java index f52b57ae9b..4e024f623b 100644 --- a/source/java/org/alfresco/service/cmr/module/ModuleDetails.java +++ b/source/java/org/alfresco/service/cmr/module/ModuleDetails.java @@ -38,6 +38,7 @@ public interface ModuleDetails extends Serializable static final String PROP_VERSION = "module.version"; static final String PROP_TITLE = "module.title"; static final String PROP_DESCRIPTION = "module.description"; + static final String PROP_EDITIONS = "module.editions"; static final String PROP_REPO_VERSION_MIN = "module.repo.version.min"; static final String PROP_REPO_VERSION_MAX = "module.repo.version.max"; static final String PROP_DEPENDS_PREFIX = "module.depends."; @@ -138,4 +139,13 @@ public interface ModuleDetails extends Serializable * @param installState the module install state */ void setInstallState(ModuleInstallState installState); + + List getEditions(); + + /** + * Sets the editions of Alfresco the module is valid for + * + * @param edition comma seperated list of editions. e.g. community,Enterprise + */ + void setEditions(List editions); } diff --git a/source/test-resources/module/test.war b/source/test-resources/module/test.war index efb5ef271888383f840826b5ae970b81f69d74b9..803d4244813af421005b50611d70960e6257091b 100644 GIT binary patch literal 9022 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