From 645705a1fdbd8fd8602db9633f5ec237fee75763 Mon Sep 17 00:00:00 2001 From: Alexander Falkenstern Date: Thu, 4 Nov 2021 14:49:42 +0100 Subject: [PATCH 1/2] Introduce tar/untar steps including gzip compression. --- docs/STEPS.md | 4 + .../steps/AbstractFileCompressStep.java | 117 +++++++ .../steps/AbstractFileDeCompressStep.java | 99 ++++++ .../utility/steps/AbstractFileOrTextStep.java | 23 +- .../utility/steps/AbstractFileStep.java | 28 ++ .../pipeline/utility/steps/tar/TarStep.java | 104 +++++++ .../utility/steps/tar/TarStepExecution.java | 155 ++++++++++ .../pipeline/utility/steps/tar/UnTarStep.java | 86 ++++++ .../utility/steps/tar/UnTarStepExecution.java | 243 +++++++++++++++ .../pipeline/utility/steps/zip/UnZipStep.java | 105 +------ .../utility/steps/zip/UnZipStepExecution.java | 25 +- .../pipeline/utility/steps/zip/ZipStep.java | 121 +------- .../utility/steps/zip/ZipStepExecution.java | 8 +- .../utility/steps/tar/TarStep/config.groovy | 54 ++++ .../steps/tar/TarStep/help-archive.html | 28 ++ .../steps/tar/TarStep/help-compress.html | 27 ++ .../utility/steps/tar/TarStep/help-dir.html | 28 ++ .../steps/tar/TarStep/help-exclude.html | 28 ++ .../utility/steps/tar/TarStep/help-file.html | 27 ++ .../utility/steps/tar/TarStep/help-glob.html | 29 ++ .../steps/tar/TarStep/help-overwrite.html | 27 ++ .../utility/steps/tar/TarStep/help.html | 27 ++ .../utility/steps/tar/UnTarStep/config.groovy | 47 +++ .../utility/steps/tar/UnTarStep/help-dir.html | 28 ++ .../steps/tar/UnTarStep/help-file.html | 27 ++ .../steps/tar/UnTarStep/help-glob.html | 29 ++ .../steps/tar/UnTarStep/help-quiet.html | 30 ++ .../steps/tar/UnTarStep/help-test.html | 30 ++ .../utility/steps/tar/UnTarStep/help.html | 27 ++ .../utility/steps/tar/TarStepTest.java | 287 ++++++++++++++++++ .../utility/steps/tar/UnTarStepTest.java | 250 +++++++++++++++ .../utility/steps/tar/test_broken.tar | Bin 0 -> 2560 bytes .../utility/steps/tar/test_broken.tar.gz | Bin 0 -> 281 bytes .../pipeline/utility/steps/tar/test_ok.tar | Bin 0 -> 2560 bytes .../pipeline/utility/steps/tar/test_ok.tar.gz | Bin 0 -> 281 bytes 35 files changed, 1885 insertions(+), 263 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCompressStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-archive.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-compress.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-dir.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-exclude.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-file.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-glob.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-overwrite.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/config.groovy create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-dir.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-file.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-glob.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-quiet.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-test.html create mode 100644 src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help.html create mode 100644 src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java create mode 100644 src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepTest.java create mode 100644 src/test/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/test_broken.tar create mode 100644 src/test/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/test_broken.tar.gz create mode 100644 src/test/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/test_ok.tar create mode 100644 src/test/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/test_ok.tar.gz diff --git a/docs/STEPS.md b/docs/STEPS.md index 767ff425..31f75f05 100644 --- a/docs/STEPS.md +++ b/docs/STEPS.md @@ -10,6 +10,10 @@ * `verifySha256` - Verifies the SHA-256 of a given file. ([help](../src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/fs/FileVerifySha256Step/help.html)) * `tee` - Tee output to file +### Tar/tar.gz/tgz Files +* `tar` - Create Tar file. ([help](../src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help.html)) +* `untar` - Extract Tar file ([help](../src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help.html)) + ### Zip Files * `zip` - Create Zip file. ([help](../src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep/help.html)) * `unzip` - Extract/Read Zip file ([help](../src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep/help.html)) diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCompressStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCompressStep.java new file mode 100644 index 00000000..0ed700d8 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCompressStep.java @@ -0,0 +1,117 @@ +package org.jenkinsci.plugins.pipeline.utility.steps; + +import org.kohsuke.stapler.DataBoundSetter; + +public abstract class AbstractFileCompressStep extends AbstractFileStep { + private String dir; + private String glob; + private String exclude; + private boolean archive = false; + private boolean overwrite = false; + + /** + * The relative path of the base directory to create the archive from. + * Leave empty to create from the current working directory. + * + * @return the dir + */ + public String getDir() { + return dir; + } + + /** + * The relative path of the base directory to create the archive from. + * Leave empty to create from the current working directory. + * + * @param dir the dir + */ + @DataBoundSetter + public void setDir(String dir) { + this.dir = dir; + } + + /** + * Ant style pattern + * of files to include in the archive. + * Leave empty to include all files and directories. + * + * @return the include pattern + */ + public String getGlob() { + return glob; + } + + /** + * Ant style pattern + * of files to include in the archive. + * Leave empty to include all files and directories. + * + * @param glob the include pattern + */ + @DataBoundSetter + public void setGlob(String glob) { + this.glob = glob; + } + + /** + * Ant style pattern + * of files to exclude from the archive. + * + * @return the exclude pattern + */ + public String getExclude() { + return exclude; + } + + /** + * Ant style pattern + * of files to exclude in the archive. + * + * @param exclude the exclude pattern + */ + @DataBoundSetter + public void setExclude(String exclude) { + this.exclude = exclude; + } + + /** + * If the archive file should be archived as an artifact of the current build. + * The file will still be kept in the workspace after archiving. + * + * @return if it should be archived or not + */ + public boolean isArchive() { + return archive; + } + + /** + * If the archive file should be archived as an artifact of the current build. + * The file will still be kept in the workspace after archiving. + * + * @param archive if it should be archived or not + */ + @DataBoundSetter + public void setArchive(boolean archive) { + this.archive = archive; + } + + /** + * If the archive file should be overwritten in case of already existing a file with the same name. + * + * @return if the file should be overwritten or not in case of existing. + */ + public boolean isOverwrite() { + return overwrite; + } + + /** + * If the archive file should be overwritten in case of already existing a file with the same name. + * + * @param overwrite if the file should be overwritten or not in case of existing. + */ + @DataBoundSetter + public void setOverwrite(boolean overwrite) { + this.overwrite = overwrite; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java new file mode 100644 index 00000000..7732fcac --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java @@ -0,0 +1,99 @@ +package org.jenkinsci.plugins.pipeline.utility.steps; + +import org.kohsuke.stapler.DataBoundSetter; + +public abstract class AbstractFileDeCompressStep extends AbstractFileStep { + private String dir; + private String glob; + private boolean test = false; + private boolean quiet = false; + + /** + * The relative path of the base directory to create the archive from. + * Leave empty to create from the current working directory. + * + * @return the dir + */ + public String getDir() { + return dir; + } + + /** + * The relative path of the base directory to create the archive from. + * Leave empty to create from the current working directory. + * + * @param dir the dir + */ + @DataBoundSetter + public void setDir(String dir) { + this.dir = dir; + } + + /** + * Ant style pattern + * of files to extract from the archive. + * Leave empty to include all files and directories. + * + * @return the include pattern + */ + public String getGlob() { + return glob; + } + + /** + * Ant style pattern + * of files to extract from the archive. + * Leave empty to include all files and directories. + * + * @param glob the include pattern + */ + @DataBoundSetter + public void setGlob(String glob) { + this.glob = glob; + } + + /** + * Test the integrity of the archive instead of extracting it. + * When this parameter is enabled, all other parameters (except for {@link #getFile()}) will be ignored. + * The step will return true or false depending on the result + * instead of throwing an exception. + * + * @return if the archive should just be tested or not + */ + public boolean isTest() { + return test; + } + + /** + * Test the integrity of the archive instead of extracting it. + * When this parameter is enabled, all other parameters (except for {@link #getFile()}) will be ignored. + * The step will return true or false depending on the result + * instead of throwing an exception. + * + * @param test if the archive should just be tested or not + */ + @DataBoundSetter + public void setTest(boolean test) { + this.test = test; + } + + /** + * Suppress the verbose output that logs every single file that is dealt with. + * + * @return if verbose logging should be suppressed + */ + public boolean isQuiet() { + return quiet; + } + + /** + * Suppress the verbose output that logs every single file that is dealt with. + * + * @param quiet if verbose logging should be suppressed + */ + @DataBoundSetter + public void setQuiet(boolean quiet) { + this.quiet = quiet; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileOrTextStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileOrTextStep.java index 50864e3b..9991acec 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileOrTextStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileOrTextStep.java @@ -1,31 +1,10 @@ package org.jenkinsci.plugins.pipeline.utility.steps; -import org.jenkinsci.plugins.workflow.steps.Step; import org.kohsuke.stapler.DataBoundSetter; -public abstract class AbstractFileOrTextStep extends Step { - protected String file; +public abstract class AbstractFileOrTextStep extends AbstractFileStep { protected String text; - /** - * The path to a file in the workspace to read from. - * - * @return the path - */ - public String getFile() { - return file; - } - - /** - * The path to a file in the workspace to read from. - * - * @param file the path - */ - @DataBoundSetter - public void setFile(String file) { - this.file = file; - } - /** * A String containing the formatted data. * diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileStep.java new file mode 100644 index 00000000..9bcac5b7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileStep.java @@ -0,0 +1,28 @@ +package org.jenkinsci.plugins.pipeline.utility.steps; + +import org.jenkinsci.plugins.workflow.steps.Step; +import org.kohsuke.stapler.DataBoundSetter; + +public abstract class AbstractFileStep extends Step { + private String file; + + /** + * The path to a file in the workspace to read from. + * + * @return the path + */ + public String getFile() { + return file; + } + + /** + * The path to a file in the workspace to read from. + * + * @param file the path + */ + @DataBoundSetter + public void setFile(String file) { + this.file = file; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java new file mode 100644 index 00000000..48eb8964 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java @@ -0,0 +1,104 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jenkinsci.plugins.pipeline.utility.steps.tar; + +import com.google.common.collect.ImmutableSet; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.Descriptor; +import hudson.model.TaskListener; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileCompressStep; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; + +import java.util.Set; + +/** + * Creates a tar file. + * + * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. + */ +public class TarStep extends AbstractFileCompressStep { + private boolean compress = true; + + @DataBoundConstructor + public TarStep(String file) throws Descriptor.FormException { + if (StringUtils.isBlank(file)) { + throw new Descriptor.FormException("Can not be empty", "file"); + } + setFile(file); + } + + /** + * If the tar file should be compressed with gzip. + * + * @return if tar should be compressed with gzip + */ + public boolean isCompress() { + return compress; + } + + /** + * If the tar file should be compressed with gzip. + * + * @param compress if it should be compressed with gz or not + */ + @DataBoundSetter + public void setCompress(boolean compress) { + this.compress = compress; + } + + + @Override + public StepExecution start(StepContext context) throws Exception { + return new TarStepExecution(this, context); + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + + public DescriptorImpl() { + + } + + @Override + public Set> getRequiredContext() { + return ImmutableSet.of(TaskListener.class, FilePath.class); + } + + @Override + public String getFunctionName() { + return "tar"; + } + + @Override + public String getDisplayName() { + return "Create Tar file"; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java new file mode 100644 index 00000000..75ddd16a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java @@ -0,0 +1,155 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps.tar; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.FilePath; +import hudson.Launcher; +import hudson.Util; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.remoting.VirtualChannel; +import hudson.util.io.Archiver; +import hudson.util.io.ArchiverFactory; +import jenkins.MasterToSlaveFileCallable; +import jenkins.util.BuildListenerAdapter; +import org.apache.commons.lang.StringUtils; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.types.FileSet; +import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +/** + * Execution of {@link TarStep}. + * + * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. + */ +public class TarStepExecution extends SynchronousNonBlockingStepExecution { + private static final long serialVersionUID = 1L; + + private transient TarStep step; + + protected TarStepExecution(@NonNull TarStep step, @NonNull StepContext context) { + super(context); + this.step = step; + } + + @Override + protected Void run() throws Exception { + TaskListener listener = getContext().get(TaskListener.class); + assert listener != null; + FilePath ws = getContext().get(FilePath.class); + assert ws != null; + FilePath source = ws; + if (!StringUtils.isBlank(step.getDir())) { + source = ws.child(step.getDir()); + if (!source.exists()) { + throw new IOException(source.getRemote() + " does not exist."); + } else if (!source.isDirectory()) { + throw new IOException(source.getRemote() + " is not a directory."); + } + } + FilePath destination = ws.child(step.getFile()); + if (destination.exists() && !step.isOverwrite()) { + throw new IOException(destination.getRemote() + " exists."); + } + if (StringUtils.isBlank(step.getGlob()) && StringUtils.isBlank(step.getExclude())) { + listener.getLogger().println("Writing tar file of " + source.getRemote() + " to " + destination.getRemote()); + } else { + listener.getLogger().println("Writing tar file of " + source.getRemote() + + " filtered by [" + step.getGlob() + "] - [" + step.getExclude() + "] to " + destination.getRemote()); + } + int count = source.act(new TarItFileCallable(destination, step.getGlob(), step.getExclude(), step.isCompress(), step.isOverwrite())); + listener.getLogger().println("Tared " + count + " entries."); + if (step.isArchive()) { + Run build = getContext().get(Run.class); + if (build == null) { + throw new MissingContextVariableException(Run.class); + } + Launcher launcher = getContext().get(Launcher.class); + if (launcher == null) { + throw new MissingContextVariableException(Launcher.class); + } + listener.getLogger().println("Archiving " + destination.getRemote()); + Map files = new HashMap<>(); + String s = step.getFile().replace('\\', '/'); + files.put(s, s); + build.pickArtifactManager().archive(ws, launcher, new BuildListenerAdapter(listener), files); + } + + return null; + } + + /** + * Performs the actual tar operation on the slave where the source dir is located. + */ + static class TarItFileCallable extends MasterToSlaveFileCallable { + final FilePath tarFile; + final String glob; + final String exclude; + final boolean compress; + final boolean overwrite; + + public TarItFileCallable(FilePath tarFile, String glob, String exclude, boolean compress, boolean overwrite) { + this.tarFile = tarFile; + this.glob = StringUtils.isBlank(glob) ? "**/*" : glob; + this.exclude = exclude; + this.compress = compress; + this.overwrite = overwrite; + } + + @Override + public Integer invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + Path p = Paths.get(tarFile.getRemote()); + if (overwrite && Files.exists(p)) { + Files.delete(p); //Will throw exception if it fails to delete it + } + + Archiver archiver = (compress ? ArchiverFactory.TARGZ : ArchiverFactory.TAR).create(tarFile.write()); + FileSet fileSet = Util.createFileSet(dir, glob, exclude); + DirectoryScanner scanner = fileSet.getDirectoryScanner(new org.apache.tools.ant.Project()); + try { + for (String path : scanner.getIncludedFiles()) { + File toArchive = new File(dir, path).getCanonicalFile(); + if (!Files.isSameFile(toArchive.toPath(), p)) { + archiver.visit(toArchive, path); + } + } + } finally { + archiver.close(); + } + return archiver.countEntries(); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java new file mode 100644 index 00000000..274e50cd --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java @@ -0,0 +1,86 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps.tar; + +import com.google.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.Descriptor; +import hudson.model.TaskListener; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileDeCompressStep; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.StepExecution; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.Set; + +/** + * Unzips a tar file. + * Can also be used to test a tar file. + * + * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. + */ +public class UnTarStep extends AbstractFileDeCompressStep { + + @DataBoundConstructor + public UnTarStep(String file) throws Descriptor.FormException { + if (StringUtils.isBlank(file)) { + throw new Descriptor.FormException("Can not be empty", "file"); + } + setFile(file); + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new UnTarStepExecution(this, context); + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + + public DescriptorImpl() { + + } + + @Override + public Set> getRequiredContext() { + return ImmutableSet.of(TaskListener.class, FilePath.class); + } + + @Override + public String getFunctionName() { + return "untar"; + } + + @Override + @NonNull + public String getDisplayName() { + return "Extract Tar file"; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java new file mode 100644 index 00000000..f82db7a1 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java @@ -0,0 +1,243 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps.tar; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.FilePath; +import hudson.model.TaskListener; +import hudson.remoting.VirtualChannel; +import jenkins.MasterToSlaveFileCallable; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.tools.ant.types.selectors.SelectorUtils; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.channels.FileChannel; + +/** + * The execution of a {@link UnTarStep}. + * + * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. + */ +public class UnTarStepExecution extends SynchronousNonBlockingStepExecution { + private static final long serialVersionUID = 1L; + + private transient UnTarStep step; + + protected UnTarStepExecution(@NonNull UnTarStep step, @NonNull StepContext context) { + super(context); + this.step = step; + } + + @Override + protected Object run() throws Exception { + TaskListener listener = getContext().get(TaskListener.class); + assert listener != null; + FilePath ws = getContext().get(FilePath.class); + assert ws != null; + if (step.isTest()) { + return test(); + } + FilePath source = ws.child(step.getFile()); + if (!source.exists()) { + throw new IOException(source.getRemote() + " does not exist."); + } else if (source.isDirectory()) { + throw new IOException(source.getRemote() + " is a directory."); + } + FilePath destination = ws; + if (!StringUtils.isBlank(step.getDir())) { + destination = ws.child(step.getDir()); + } + + return source.act(new UnTarFileCallable(listener, destination, step.getGlob(), step.isQuiet())); + } + + private Boolean test() throws IOException, InterruptedException { + TaskListener listener = getContext().get(TaskListener.class); + assert listener != null; + FilePath ws = getContext().get(FilePath.class); + assert ws != null; + FilePath source = ws.child(step.getFile()); + if (!source.exists()) { + listener.error(source.getRemote() + " does not exist."); + return false; + } else if (source.isDirectory()) { + listener.error(source.getRemote() + " is a directory."); + return false; + } + return source.act(new TestTarFileCallable(listener)); + } + + /** + * Performs the untar on the slave where the tar file is located. + */ + public static class UnTarFileCallable extends MasterToSlaveFileCallable { + private final TaskListener listener; + private final FilePath destination; + private final String glob; + private final boolean quiet; + + public UnTarFileCallable(TaskListener listener, FilePath destination, String glob, boolean quiet) { + this.listener = listener; + this.destination = destination; + this.glob = glob; + this.quiet = quiet; + } + + @Override + public Void invoke(File tarFile, VirtualChannel channel) throws IOException, InterruptedException { + PrintStream logger = listener.getLogger(); + boolean doGlob = !StringUtils.isBlank(glob); + + InputStream fileStream = new FileInputStream(tarFile); + + try { + //check if matches standard gzip magic number + fileStream = new GzipCompressorInputStream(fileStream); + } catch (IOException exception) { + // Eat exception, may be not compressed file + } + + destination.mkdirs(); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream(fileStream)) { + logger.println("Extracting from " + tarFile.getAbsolutePath()); + TarArchiveEntry entry; + Integer fileCount = 0; + while ((entry = tarStream.getNextTarEntry()) != null) { + if (doGlob && !matches(entry.getName(), glob)) { + continue; + } + + FilePath f = destination.child(entry.getName()); + if (entry.isDirectory()) { + f.mkdirs(); + } else { + fileCount++; + if (!quiet) { + logger.printf("Extracting: %s -> %s%n", entry.getName(), f.getRemote()); + } + + if (entry.isCheckSumOK()) { + OutputStream outputStream = f.write(); + IOUtils.copy(tarStream, outputStream); + outputStream.close(); + } else { + throw new IOException("Not a tar archive"); + } + } + } + logger.printf("Extracted: %d files%n", fileCount); + } + finally { + logger.flush(); + } + return null; + } + + boolean matches(String path, String glob) { + String safeGlob = glob.replace('/', File.separatorChar); + String safePath = path.replace('/', File.separatorChar); + return SelectorUtils.matchPath(safeGlob, safePath); + } + } + + /** + * Performs a test of a tar file on the slave where the file is. + */ + static class TestTarFileCallable extends MasterToSlaveFileCallable { + private TaskListener listener; + + public TestTarFileCallable(TaskListener listener) { + this.listener = listener; + } + + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + PrintStream logger = listener.getLogger(); + + FileInputStream fileStream = new FileInputStream(f); + FileChannel fileChannel = fileStream.getChannel(); + logger.printf("Checking %d bytes in %s%n", f.length(), f.getAbsolutePath()); + + byte[] signature = new byte[2]; + try { + fileStream.read(signature); + fileChannel.position(0); + } catch (IOException exception) { + fileStream.close(); + listener.error("Error validating tar/tgz file: " + exception.getMessage()); + return false; + } finally { + logger.flush(); + } + + InputStream inputStream = fileStream; + if(GzipCompressorInputStream.matches(signature, signature.length)) { + try { + inputStream = new GzipCompressorInputStream(inputStream); + int nRead = -1; + do { + byte[] buffer = new byte[4096]; + nRead = inputStream.read(buffer); + } while (nRead >= 0); + fileChannel.position(0); + } catch (IOException exception) { + inputStream.close(); + listener.error("Error validating tgz file: " + exception.getMessage()); + return false; + } finally { + logger.flush(); + } + } + + TarArchiveInputStream tarStream = new TarArchiveInputStream(inputStream); + try { + TarArchiveEntry entry; + while ((entry = tarStream.getNextTarEntry()) != null) { + if (!entry.isCheckSumOK()) { + throw new IOException("Not a tar archive"); + } + } + } catch (IOException exception) { + listener.error("Error validating tar file: " + exception.getMessage()); + return false; + } finally { + tarStream.close(); + logger.flush(); + } + return true; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java index 93bd13b2..0593462d 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java @@ -31,7 +31,7 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileDeCompressStep; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; @@ -46,22 +46,17 @@ * * @author Robert Sandell <rsandell@cloudbees.com>. */ -public class UnZipStep extends Step { +public class UnZipStep extends AbstractFileDeCompressStep { - private final String zipFile; - private String dir; private String charset; - private String glob; - private boolean test = false; private boolean read = false; - private boolean quiet = false; @DataBoundConstructor public UnZipStep(String zipFile) throws Descriptor.FormException { if (StringUtils.isBlank(zipFile)) { throw new Descriptor.FormException("Can not be empty", "zipFile"); } - this.zipFile = zipFile; + setFile(zipFile); } /** @@ -69,76 +64,7 @@ public UnZipStep(String zipFile) throws Descriptor.FormException { * @return the path */ public String getZipFile() { - return zipFile; - } - - /** - * The relative path of the base directory to create the zip from. - * Leave empty to create from the current working directory. - * - * @return the dir - */ - public String getDir() { - return dir; - } - - /** - * The relative path of the base directory to create the zip from. - * Leave empty to create from the current working directory. - * - * @param dir the dir - */ - @DataBoundSetter - public void setDir(String dir) { - this.dir = dir; - } - - /** - * Ant style pattern - * of files to extract from the zip. - * Leave empty to include all files and directories. - * - * @return the include pattern - */ - public String getGlob() { - return glob; - } - - /** - * Ant style pattern - * of files to extract from the zip. - * Leave empty to include all files and directories. - * - * @param glob the include pattern - */ - @DataBoundSetter - public void setGlob(String glob) { - this.glob = glob; - } - - /** - * Test the integrity of the archive instead of extracting it. - * When this parameter is enabled, all other parameters (except for {@link #getZipFile()}) will be ignored. - * The step will return true or false depending on the result - * instead of throwing an exception. - * - * @return if the archive should just be tested or not - */ - public boolean isTest() { - return test; - } - - /** - * Test the integrity of the archive instead of extracting it. - * When this parameter is enabled, all other parameters (except for {@link #getZipFile()}) will be ignored. - * The step will return true or false depending on the result - * instead of throwing an exception. - * - * @param test if the archive should just be tested or not - */ - @DataBoundSetter - public void setTest(boolean test) { - this.test = test; + return getFile(); } /** @@ -191,29 +117,6 @@ public void setCharset(String charset) this.charset = (charset.trim().isEmpty()) ? "UTF-8" : charset; } - /** - * Suppress the verbose output that logs every single file that is dealt with. - * E.g. - * unzip zipFile: 'example.zip', glob: 'version.txt', quiet: true - * - * @return if verbose logging should be suppressed - */ - public boolean isQuiet() { - return quiet; - } - - /** - * Suppress the verbose output that logs every single file that is dealt with. - * E.g. - * unzip zipFile: 'example.zip', glob: 'version.txt', quiet: true - * - * @param quiet if verbose logging should be suppressed - */ - @DataBoundSetter - public void setQuiet(boolean quiet) { - this.quiet = quiet; - } - @Override public StepExecution start(StepContext context) throws Exception { return new UnZipStepExecution(this, context); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java index 17d0dc25..013fc658 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java @@ -74,7 +74,7 @@ protected Object run() throws Exception { if (step.isTest()) { return test(); } - FilePath source = ws.child(step.getZipFile()); + FilePath source = ws.child(step.getFile()); if (!source.exists()) { throw new IOException(source.getRemote() + " does not exist."); } else if (source.isDirectory()) { @@ -92,7 +92,7 @@ private Boolean test() throws IOException, InterruptedException { assert listener != null; FilePath ws = getContext().get(FilePath.class); assert ws != null; - FilePath source = ws.child(step.getZipFile()); + FilePath source = ws.child(step.getFile()); if (!source.exists()) { listener.error(source.getRemote() + " does not exist."); return false; @@ -150,10 +150,7 @@ public Map invoke(File zipFile, VirtualChannel channel) throws I if (!read) { if (!quiet) { - logger.print("Extracting: "); - logger.print(entry.getName()); - logger.print(" -> "); - logger.println(f.getRemote()); + logger.printf("Extracting: %s -> %s%n", entry.getName(), f.getRemote()); } /* @@ -168,8 +165,7 @@ public Map invoke(File zipFile, VirtualChannel channel) throws I } } else { if (!quiet) { - logger.print("Reading: "); - logger.println(entry.getName()); + logger.printf("Reading: %s%n", entry.getName()); } try (InputStream is = zip.getInputStream(entry)) { @@ -179,14 +175,10 @@ public Map invoke(File zipFile, VirtualChannel channel) throws I } } if (read) { - logger.print("Read: "); - logger.print(fileCount); - logger.println(" files"); + logger.printf("Read: %d files%n", fileCount); return strMap; } else { - logger.print("Extracted: "); - logger.print(fileCount); - logger.println(" files"); + logger.printf("Extracted: %d files%n", fileCount); return null; } } finally { @@ -215,10 +207,7 @@ public TestZipFileCallable(TaskListener listener) { public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { PrintStream logger = listener.getLogger(); try (ZipFile zip = new ZipFile(f)) { - logger.print("Checking "); - logger.print(zip.size()); - logger.print(" zipped entries in "); - logger.println(f.getAbsolutePath()); + logger.printf("Checking %d zipped entries in %s%n", zip.size(), f.getAbsolutePath()); Checksum checksum = new CRC32(); byte[] buffer = new byte[4096]; diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java index 601a2e26..2b5ff563 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java @@ -29,12 +29,11 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileCompressStep; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.DataBoundSetter; import java.util.Set; @@ -43,20 +42,14 @@ * * @author Robert Sandell <rsandell@cloudbees.com>. */ -public class ZipStep extends Step { - private final String zipFile; - private String dir; - private String glob; - private String exclude; - private boolean archive = false; - private boolean overwrite = false; +public class ZipStep extends AbstractFileCompressStep { @DataBoundConstructor public ZipStep(String zipFile) throws Descriptor.FormException { if (StringUtils.isBlank(zipFile)) { throw new Descriptor.FormException("Can not be empty", "zipFile"); } - this.zipFile = zipFile; + setFile(zipFile); } /** @@ -65,115 +58,9 @@ public ZipStep(String zipFile) throws Descriptor.FormException { * @return the path */ public String getZipFile() { - return zipFile; + return getFile(); } - /** - * The relative path of the base directory to create the zip from. - * Leave empty to create from the current working directory. - * - * @return the dir - */ - public String getDir() { - return dir; - } - - /** - * The relative path of the base directory to create the zip from. - * Leave empty to create from the current working directory. - * - * @param dir the dir - */ - @DataBoundSetter - public void setDir(String dir) { - this.dir = dir; - } - - /** - * Ant style pattern - * of files to include in the zip. - * Leave empty to include all files and directories. - * - * @return the include pattern - */ - public String getGlob() { - return glob; - } - - /** - * Ant style pattern - * of files to include in the zip. - * Leave empty to include all files and directories. - * - * @param glob the include pattern - */ - @DataBoundSetter - public void setGlob(String glob) { - this.glob = glob; - } - - /** - * Ant style pattern - * of files to exclude from the zip. - * - * @return the exclude pattern - */ - public String getExclude() { - return exclude; - } - - /** - * Ant style pattern - * of files to exclude in the zip. - * - * @param exclude the exclude pattern - */ - @DataBoundSetter - public void setExclude(String exclude) { - this.exclude = exclude; - } - - /** - * If the zip file should be archived as an artifact of the current build. - * The file will still be kept in the workspace after archiving. - * - * @return if it should be archived or not - */ - public boolean isArchive() { - return archive; - } - - /** - * If the zip file should be archived as an artifact of the current build. - * The file will still be kept in the workspace after archiving. - * - * @param archive if it should be archived or not - */ - @DataBoundSetter - public void setArchive(boolean archive) { - this.archive = archive; - } - - /** - * If the zip file should be overwritten in case of already existing a file with the same name. - * - * @return if the file should be overwritten or not in case of existing. - */ - public boolean isOverwrite() { - return overwrite; - } - - /** - * If the zip file should be overwritten in case of already existing a file with the same name. - * - * @param overwrite if the file should be overwritten or not in case of existing. - */ - @DataBoundSetter - public void setOverwrite(boolean overwrite) { - this.overwrite = overwrite; - } - - @Override public StepExecution start(StepContext context) throws Exception { return new ZipStepExecution(this, context); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java index 87604bff..2917d4ac 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java @@ -80,7 +80,7 @@ protected Void run() throws Exception { throw new IOException(source.getRemote() + " is not a directory."); } } - FilePath destination = ws.child(step.getZipFile()); + FilePath destination = ws.child(step.getFile()); if (destination.exists() && !step.isOverwrite()) { throw new IOException(destination.getRemote() + " exists."); } @@ -103,7 +103,7 @@ protected Void run() throws Exception { } listener.getLogger().println("Archiving " + destination.getRemote()); Map files = new HashMap<>(); - String s = step.getZipFile().replace('\\', '/'); + String s = step.getFile().replace('\\', '/'); files.put(s, s); build.pickArtifactManager().archive(ws, launcher, new BuildListenerAdapter(listener), files); } @@ -132,9 +132,7 @@ public ZipItFileCallable(FilePath zipFile, String glob, String exclude, boolean @Override public Integer invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - String zipFileRemotePath = zipFile.getRemote(); - - Path p = Paths.get(zipFileRemotePath); + Path p = Paths.get(zipFile.getRemote()); if (overwrite && Files.exists(p)) { Files.delete(p); //Will throw exception if it fails to delete it } diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/config.groovy b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/config.groovy new file mode 100644 index 00000000..c243c21b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/config.groovy @@ -0,0 +1,54 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jenkinsci.plugins.pipeline.utility.steps.zip.ZipStep + +def f = namespace(lib.FormTagLib) as lib.FormTagLib + +f.entry(field: 'file', title: _('Tar File')) { + f.textbox() +} + +f.entry(field: 'dir', title: _('Directory')) { + f.textbox() +} + +f.entry(field: 'compress', title: _('Compress')) { + f.textbox() +} + +f.entry(field: 'glob', title: _('Glob')) { + f.textbox() +} + +f.entry(field: 'exclude', title: _('Exclude')) { + f.textbox() +} + +f.entry(field: 'archive', title: _('Archive as artifact')) { + f.checkbox() +} + +f.entry(field: 'overwrite', title: _('Overwrite')) { + f.checkbox() +} diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-archive.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-archive.html new file mode 100644 index 00000000..bcd92c3f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-archive.html @@ -0,0 +1,28 @@ + + +

+ If the tar file should be archived as an artifact of the current build. + The file will still be kept in the workspace after archiving. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-compress.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-compress.html new file mode 100644 index 00000000..c778dd77 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-compress.html @@ -0,0 +1,27 @@ + + +

+ The created tar file shall be compressed as gz. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-dir.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-dir.html new file mode 100644 index 00000000..0d4a71f0 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-dir.html @@ -0,0 +1,28 @@ + + +

+ The path of the base directory to create the tar from. + Leave empty to create from the current working directory. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-exclude.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-exclude.html new file mode 100644 index 00000000..f14a0691 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-exclude.html @@ -0,0 +1,28 @@ + + +

+ Ant style pattern + of files to exclude from the tar. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-file.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-file.html new file mode 100644 index 00000000..626d0978 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-file.html @@ -0,0 +1,27 @@ + + +

+ The name/path of the tar file to create. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-glob.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-glob.html new file mode 100644 index 00000000..b70f7f1a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-glob.html @@ -0,0 +1,29 @@ + + +

+ Ant style pattern + of files to include in the tar. + Leave empty to include all files and directories. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-overwrite.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-overwrite.html new file mode 100644 index 00000000..21717f61 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help-overwrite.html @@ -0,0 +1,27 @@ + + +

+ If the tar file should be overwritten in case of already existing a file with the same name. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help.html new file mode 100644 index 00000000..1b7a0f3f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep/help.html @@ -0,0 +1,27 @@ + + +

+ Create a tar/tar.gz file of content in the workspace. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/config.groovy b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/config.groovy new file mode 100644 index 00000000..81f5e540 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/config.groovy @@ -0,0 +1,47 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +package org.jenkinsci.plugins.pipeline.utility.steps.zip.UnZipStep + +def f = namespace(lib.FormTagLib) as lib.FormTagLib + +f.entry(field: 'file', title: _('Tar File')) { + f.textbox() +} + +f.entry(field: 'dir', title: _('Directory')) { + f.textbox() +} + +f.entry(field: 'glob', title: _('Glob')) { + f.textbox() +} +f.entry(field: 'test', title: _('Test the archive')) { + f.checkbox() +} + +f.entry(field: 'quiet', title: _('Suppress logging of each file')) { + f.checkbox() +} diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-dir.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-dir.html new file mode 100644 index 00000000..7a8ece7e --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-dir.html @@ -0,0 +1,28 @@ + + +

+ The path of the base directory to extract the tar to. + Leave empty to extract in the current working directory. +

diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-file.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-file.html new file mode 100644 index 00000000..fc3c5941 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-file.html @@ -0,0 +1,27 @@ + + +

+ The name/path of the tar/tar.gz file to extract. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-glob.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-glob.html new file mode 100644 index 00000000..3baa2c3c --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-glob.html @@ -0,0 +1,29 @@ + + +

+ Ant style pattern + of files to extract from the tar. + Leave empty to include all files and directories. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-quiet.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-quiet.html new file mode 100644 index 00000000..cd47e8f8 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-quiet.html @@ -0,0 +1,30 @@ + +

+ Suppress the verbose output that logs every single file that is dealt with. + E.g. + + untar file: 'example.tgz', quiet: true + +

diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-test.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-test.html new file mode 100644 index 00000000..a87a887a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help-test.html @@ -0,0 +1,30 @@ + + +

+ Test the integrity of the archive instead of extracting it. + When this parameter is enabled, all other parameters (except for file) will be ignored. + The step will return true or false depending on the result + instead of throwing an exception. +

\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help.html b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help.html new file mode 100644 index 00000000..93f25639 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep/help.html @@ -0,0 +1,27 @@ + + +

+ Extract a tar/tar.gz file in the workspace. +

\ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java new file mode 100644 index 00000000..e5a4a3d8 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java @@ -0,0 +1,287 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps.tar; + +import hudson.model.Label; +import hudson.model.Result; +import hudson.model.Run; +import jenkins.util.VirtualFile; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.IOException; +import java.util.Scanner; + +import static org.junit.Assert.*; + +/** + * Tests for {@link TarStep}. + * + * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. + */ +public class TarStepTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Before + public void setup() throws Exception { + j.createOnlineSlave(Label.get("slaves")); + } + + @Test + public void configRoundTrip() throws Exception { + TarStep step = new TarStep("target/my.tgz"); + step.setDir("base/"); + step.setGlob("**/*.tgz"); + step.setExclude("**/*.txt"); + step.setArchive(true); + step.setCompress(true); + step.setOverwrite(true); + + TarStep step2 = new StepConfigTester(j).configRoundTrip(step); + j.assertEqualDataBoundBeans(step, step2); + } + + @Test + public void simpleArchivedTar() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.txt', text: 'Hello Outer World!'\n" + + " dir('hello') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " }\n" + + " tar file: 'hello.tar', dir: 'hello', archive: true, compress: false\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogContains("Writing tar file", run); + j.assertLogContains("Archiving", run); + verifyArchivedHello(run, ""); + } + + @Test + public void shouldNotPutOutputArchiveIntoItself() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {" + + " writeFile file: 'hello.txt', text: 'Hello world'\n" + + " tar file: 'output.tgz', dir: '', glob: '', archive: true, overwrite: true\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogContains("Writing tar file", run); + verifyArchivedNotContainingItself(run); + } + + @Test + public void shouldNotPutOutputArchiveIntoItself_nonCanonicalPath() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {" + + " dir ('src') {\n" + + " writeFile file: 'hello.txt', text: 'Hello world'\n" + + " }\n" + + " tar file: 'src/../src/output.tgz', dir: '', glob: '', archive: true\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogContains("Writing tar file", run); + verifyArchivedNotContainingItself(run); + } + + @Test + public void canArchiveFileWithSameName() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " dir ('src') {\n" + + " writeFile file: 'hello.txt', text: 'Hello world'\n" + + " writeFile file: 'output.tgz', text: 'not really a tar'\n" + + " }\n" + + " dir ('out') {\n" + + " tar file: 'output.tgz', dir: '../src', glob: '', archive: true, overwrite: true\n" + + " }\n" + + "}\n", + true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + + j.assertLogContains("Writing tar file", run); + assertTrue("Build should have artifacts", run.getHasArtifacts()); + Run.Artifact artifact = run.getArtifacts().get(0); + assertEquals("output.tgz", artifact.getFileName()); + VirtualFile file = run.getArtifactManager().root().child(artifact.relativePath); + try (GzipCompressorInputStream compressor = new GzipCompressorInputStream(file.open()); + TarArchiveInputStream tar = new TarArchiveInputStream(compressor)) { + ArchiveEntry entry = tar.getNextEntry(); + while (entry != null && !entry.getName().equals("output.tgz")) { + System.out.println("zip entry name is: " + entry.getName()); + entry = tar.getNextEntry(); + } + assertNotNull("output.tgz should be included in the tgz", entry); + // we should have the tgz - but double check + assertEquals("output.tgz", entry.getName()); + Scanner scanner = new Scanner(tar); + assertTrue(scanner.hasNextLine()); + // the file that was not a tar should be included. + assertEquals("not really a tar", scanner.nextLine()); + } + } + + @Test + public void globbedArchivedTar() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.outer', text: 'Hello Outer World!'\n" + + " dir('hello') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " }\n" + + " tar file: 'hello.tar', glob: '**/*.txt', archive: true, compress: false\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + verifyArchivedHello(run, "hello/"); + } + + @Test + public void excludedPatternWithAll() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " writeFile file: 'goodbye.txt', text: 'Goodbye World!'\n" + + " tar file: 'hello.tar', exclude: 'goodbye.txt', archive: true, compress: false\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + verifyArchivedHello(run, ""); + + } + + @Test + public void excludedPatternWithGlob() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " writeFile file: 'goodbye.txt', text: 'Goodbye World!'\n" + + " tar file: 'hello.tar', glob: '*.txt', exclude: 'goodbye.txt', archive: true, compress: false\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + verifyArchivedHello(run, ""); + } + + @Test + public void emptyTarFile() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " tar file: '', glob: '**/*.txt', archive: true\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + j.assertLogContains("Can not be empty", run); + } + + @Test + public void existingTarFileWithoutOverwrite() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.tar.gz', text: 'Hello Tar!'\n" + + " tar file: 'hello.tar.gz', glob: '**/*.txt', archive: true\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + j.assertLogContains("hello.tar.gz exists.", run); + + } + + @Test + public void existingTarFileWithOverwrite() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.tgz', text: 'Hello Tar!'\n" + + " writeFile file: 'hello.txt', text: 'Hello world'\n" + + " tar file: 'hello.tgz', glob: '**/*.txt', archive: true, overwrite:true\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0).get()); + j.assertLogNotContains("hello.tgz exists.", run); + } + + @Test + public void noExistingTarFileWithOverwrite() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " writeFile file: 'hello.txt', text: 'Hello world'\n" + + " tar file: 'hello.tgz', glob: '**/*.txt', overwrite:true\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0).get()); + j.assertLogNotContains("java.io.IOException", run); + j.assertLogNotContains("Failed to delete", run); + j.assertLogContains("Tared 1 entries.", run); + } + + private void verifyArchivedHello(WorkflowRun run, String basePath) throws IOException { + assertTrue("Build should have artifacts", run.getHasArtifacts()); + Run.Artifact artifact = run.getArtifacts().get(0); + assertEquals("hello.tar", artifact.getFileName()); + + VirtualFile file = run.getArtifactManager().root().child(artifact.relativePath); + try (TarArchiveInputStream tar = new TarArchiveInputStream(file.open())) { + ArchiveEntry entry = tar.getNextEntry(); + while (entry.isDirectory()) { + entry = tar.getNextEntry(); + } + assertNotNull(entry); + assertEquals(basePath + "hello.txt", entry.getName()); + try (Scanner scanner = new Scanner(tar)) { + assertTrue(scanner.hasNextLine()); + assertEquals("Hello World!", scanner.nextLine()); + assertNull("There should be no more entries", tar.getNextEntry()); + } + } + } + + private void verifyArchivedNotContainingItself(WorkflowRun run) throws IOException { + assertTrue("Build should have artifacts", run.getHasArtifacts()); + + Run.Artifact artifact = run.getArtifacts().get(0); + VirtualFile file = run.getArtifactManager().root().child(artifact.relativePath); + try (GzipCompressorInputStream compressor = new GzipCompressorInputStream(file.open()); + TarArchiveInputStream tar = new TarArchiveInputStream(compressor)) { + for (ArchiveEntry entry = tar.getNextEntry(); entry != null; entry = tar.getNextEntry()) { + assertNotEquals("The zip output file shouldn't contain itself", entry.getName(), artifact.relativePath); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepTest.java b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepTest.java new file mode 100644 index 00000000..615ebb6d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepTest.java @@ -0,0 +1,250 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Alexander Falkenstern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps.tar; + +import hudson.model.Label; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.steps.StepConfigTester; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.File; +import java.net.URL; +import java.net.URLDecoder; + +import static org.jenkinsci.plugins.pipeline.utility.steps.FilenameTestsUtils.separatorsToSystemEscaped; +import static org.junit.Assert.assertFalse; + +/** + * Tests for {@link UnTarStep}. + * + * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. + */ +public class UnTarStepTest { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Before + public void setup() throws Exception { + j.createOnlineSlave(Label.get("slaves")); + } + + @Test + public void configRoundTrip() throws Exception { + UnTarStep step = new UnTarStep("target/my.tgz"); + step.setDir("base/"); + step.setGlob("**/*.tgz"); + step.setQuiet(false); + + UnTarStep step2 = new StepConfigTester(j).configRoundTrip(step); + j.assertEqualDataBoundBeans(step, step2); + assertFalse(step2.isTest()); + } + + @Test + public void simpleUntar() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " dir('compressIt') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " writeFile file: 'hello.dat', text: 'Hello World!'\n" + + " dir('two') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World2!'\n" + + " }\n" + + " tar file: '../hello.tgz'\n" + + " }\n" + + " dir('decompressIt') {\n" + + " untar '../hello.tgz'\n" + + " String txt = readFile 'hello.txt'\n" + + " echo \"Reading: ${txt}\"\n" + + " }\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogContains("Extracting: hello.txt ->", run); + j.assertLogContains("Extracting: two/hello.txt ->", run); + j.assertLogContains("Extracting: hello.dat ->", run); + j.assertLogContains("Reading: Hello World!", run); + } + + @Test + public void globUntar() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " dir('compressIt') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " writeFile file: 'hello.dat', text: 'Hello World!'\n" + + " dir('two') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World2!'\n" + + " }\n" + + " tar file: '../hello.tar.gz'\n" + + " }\n" + + " dir('decompressIt') {\n" + + " untar file: '../hello.tar.gz', glob: '**/*.txt'\n" + + " String txt = readFile 'hello.txt'\n" + + " echo \"Reading: ${txt}\"\n" + + " txt = readFile 'two/hello.txt'\n" + + " echo \"Reading: ${txt}\"\n" + + " }\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogContains("Extracting: hello.txt ->", run); + j.assertLogContains("Extracting: two/hello.txt ->", run); + j.assertLogNotContains("Extracting: hello.dat ->", run); + j.assertLogContains("Reading: Hello World!", run); + j.assertLogContains("Reading: Hello World2!", run); + } + + @Test + public void tarTest() throws Exception { + Assume.assumeTrue("Can only run in a gnu unix environment", File.pathSeparatorChar == ':'); + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " dir('compressIt') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " writeFile file: 'hello.dat', text: 'Hello Data World!'\n" + + " dir('two') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World2!'\n" + + " }\n" + + " tar file: '../hello.tgz'\n" + + " }\n" + + " sh 'head -c $(($(cat hello.tgz | wc -c) / 2)) hello.tgz > corrupt.tgz'\n" + + " def result = untar file: 'corrupt.tgz', test: true \n" + + " if (result != false)\n" + + " error('Should be corrupt!')\n" + + "}", true)); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @Test + public void testTarGZTestingBrokenTarGZ() throws Exception { + /* + This test uses a prepared tgz file that has a single flipped bit inside the byte stream of the tgz file entry. + The test method has to find this error. This will require the stream to be read, because the CRC check is able + to reveal this error. + */ + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + URL resource = getClass().getResource("test_broken.tar.gz"); + String tgz = new File(URLDecoder.decode(resource.getPath(), "UTF-8")).getAbsolutePath().replace('\\', '/'); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " def result = untar file: '" + separatorsToSystemEscaped(tgz) + "', test: true\n" + + " if (result)\n" + + " error('Should be corrupt!')\n" + + "}", true)); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @Test + public void testTarGZTestingOkayTarGZ() throws Exception { + /* + This test uses a prepared tar.gz file without any errors. + */ + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + URL resource = getClass().getResource("test_ok.tar.gz"); + String tgz = new File(URLDecoder.decode(resource.getPath(), "UTF-8")).getAbsolutePath().replace('\\', '/'); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " def result = untar file: '" + separatorsToSystemEscaped(tgz) + "', test: true\n" + + " if (!result)\n" + + " error('Should be okay!')\n" + + "}", true)); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @Test + public void testTarTestingBrokenTar() throws Exception { + /* + This test uses a prepared tgz file that has a single flipped bit inside the byte stream of the tgz file entry. + The test method has to find this error. This will require the stream to be read, because the CRC check is able + to reveal this error. + */ + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + URL resource = getClass().getResource("test_broken.tar"); + String tgz = new File(URLDecoder.decode(resource.getPath(), "UTF-8")).getAbsolutePath().replace('\\', '/'); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " def result = untar file: '" + separatorsToSystemEscaped(tgz) + "', test: true\n" + + " if (result)\n" + + " error('Should be corrupt!')\n" + + "}", true)); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @Test + public void testTarTestingOkayTar() throws Exception { + /* + This test uses a prepared tar.gz file without any errors. + */ + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + URL resource = getClass().getResource("test_ok.tar"); + String tgz = new File(URLDecoder.decode(resource.getPath(), "UTF-8")).getAbsolutePath().replace('\\', '/'); + p.setDefinition(new CpsFlowDefinition( + "node {\n" + + " def result = untar file: '" + separatorsToSystemEscaped(tgz) + "', test: true\n" + + " if (!result)\n" + + " error('Should be okay!')\n" + + "}", true)); + j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + } + + @Test + public void untarQuiet() throws Exception { + WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition( + "node('slaves') {\n" + + " dir('compressIt') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World!'\n" + + " writeFile file: 'hello.dat', text: 'Hello World!'\n" + + " dir('two') {\n" + + " writeFile file: 'hello.txt', text: 'Hello World2!'\n" + + " }\n" + + " tar file: '../hello.tgz'\n" + + " }\n" + + " dir('decompressIt') {\n" + + " untar file: '../hello.tgz', quiet: true\n" + + " String txt = readFile 'hello.txt'\n" + + " echo \"Reading: ${txt}\"\n" + + " txt = readFile 'two/hello.txt'\n" + + " echo \"Reading: ${txt}\"\n" + + " }\n" + + "}", true)); + WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); + j.assertLogNotContains("Extracting: hello.txt ->", run); + j.assertLogNotContains("Extracting: two/hello.txt ->", run); + j.assertLogNotContains("Extracting: hello.dat ->", run); + j.assertLogContains("Extracted: 3 files", run); + j.assertLogContains("Reading: Hello World!", run); + j.assertLogContains("Reading: Hello World2!", run); + } +} diff --git a/src/test/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/test_broken.tar b/src/test/resources/org/jenkinsci/plugins/pipeline/utility/steps/tar/test_broken.tar new file mode 100644 index 0000000000000000000000000000000000000000..3d50fb5da99d2a5b5314b75de82635d40d6ce31c GIT binary patch literal 2560 zcmeHG%W4BL4BY<7d_Y+2mf~~oz4Z%}wOHJ&?bMb+^7mC5NTJZ)awu7WvCwPAV_Ue( zC`TD?XwEwqLKxh|e(v7;F!;ydgMWSs&RIT~Gr--D({;3Vp=OZRz+E{HueEVT+IP)} zb*T+K2`IB{FxApoSk!_Pt!7GP1}{n42Av~fqKs0D<`X!zTqA_@-iD-dNKuPG1q*Er z1S}T-My9zCXh|P>vH{%4z^?NSbjjd&3pxK(iN=Wd!N?dN*IyWLtvR9k1x{FP@y zw;kf0CZ(@bS`vZascg-O-=(b425%)cYta%0imqfv*QHr#2ST8 zl9JdJy*-}70wE!>p`y}!n>hY?wrt@pgS?6RLqQZd zhtXXD?Ly6sR_XN*d8&;w(y?nstV?a^NkExphpCp%!lLG+XgO0VGk8hTcIX@t6J?ZI zG@rq#^s?na82m%(z~8)05>wQ>%6rtdK9G*&w`a(1=oJ=rfJfCt{1%7tyM&|b>_@pc_uX8 zYaa1xq%j#!!hw3ktb1;Aw;kf0CZ(@bS`vZascg-O-=(b425%)cYta%0imqfv*QHr#2ST8 zl9JdJy*-}70wE!>p`y}!n>hY?wrt@pgS?6RLqQZd zhtXXD?Ly6sSLyW+d8&;w(y?nstV?a^NkExphpCp%!lLG+XgO0VGk8hTcIX@t6J?ZI zG@rq#^s? Date: Fri, 5 Nov 2021 17:46:33 +0100 Subject: [PATCH 2/2] Remove duplicated code and adapt log messages. --- .../utility/steps/AbstractFileCallable.java | 40 ++++++ ...p.java => AbstractFileDecompressStep.java} | 2 +- .../utility/steps/CompressStepExecution.java | 115 ++++++++++++++++++ .../steps/DecompressStepExecution.java | 92 ++++++++++++++ .../conf/mf/ReadManifestStepExecution.java | 6 +- .../pipeline/utility/steps/tar/TarStep.java | 4 - .../utility/steps/tar/TarStepExecution.java | 72 ++--------- .../pipeline/utility/steps/tar/UnTarStep.java | 8 +- .../utility/steps/tar/UnTarStepExecution.java | 60 +++------ .../pipeline/utility/steps/zip/UnZipStep.java | 8 +- .../utility/steps/zip/UnZipStepExecution.java | 59 +++------ .../pipeline/utility/steps/zip/ZipStep.java | 4 - .../utility/steps/zip/ZipStepExecution.java | 71 ++--------- .../utility/steps/tar/TarStepTest.java | 11 +- .../utility/steps/zip/ZipStepTest.java | 10 +- 15 files changed, 320 insertions(+), 242 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCallable.java rename src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/{AbstractFileDeCompressStep.java => AbstractFileDecompressStep.java} (97%) create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/CompressStepExecution.java create mode 100644 src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/DecompressStepExecution.java diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCallable.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCallable.java new file mode 100644 index 00000000..2230457a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileCallable.java @@ -0,0 +1,40 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 CloudBees Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps; + +import hudson.FilePath; +import jenkins.MasterToSlaveFileCallable; + +public abstract class AbstractFileCallable extends MasterToSlaveFileCallable { + private FilePath destination; + + public FilePath getDestination() { + return destination; + } + + public void setDestination(FilePath destination) { + this.destination = destination; + } +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDecompressStep.java similarity index 97% rename from src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java rename to src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDecompressStep.java index 7732fcac..33620c30 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDeCompressStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/AbstractFileDecompressStep.java @@ -2,7 +2,7 @@ import org.kohsuke.stapler.DataBoundSetter; -public abstract class AbstractFileDeCompressStep extends AbstractFileStep { +public abstract class AbstractFileDecompressStep extends AbstractFileStep { private String dir; private String glob; private boolean test = false; diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/CompressStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/CompressStepExecution.java new file mode 100644 index 00000000..96a09962 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/CompressStepExecution.java @@ -0,0 +1,115 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 CloudBees Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.Run; +import hudson.model.TaskListener; +import jenkins.util.BuildListenerAdapter; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; + +/** + * Execution of {@link AbstractFileCompressStep}. + * + * @author Robert Sandell <rsandell@cloudbees.com>. + */ +public abstract class CompressStepExecution extends SynchronousNonBlockingStepExecution { + private transient AbstractFileCallable callable; + private transient final AbstractFileCompressStep step; + + protected CompressStepExecution(@NonNull AbstractFileCompressStep step, @NonNull StepContext context) { + super(context); + this.step = step; + } + + protected void setCallable(final AbstractFileCallable callable) { + this.callable = callable; + } + + @Override + protected Void run() throws Exception { + TaskListener listener = getContext().get(TaskListener.class); + assert listener != null; + + PrintStream logger = listener.getLogger(); + assert logger != null; + + FilePath ws = getContext().get(FilePath.class); + assert ws != null; + FilePath source = ws; + if (!StringUtils.isBlank(step.getDir())) { + source = ws.child(step.getDir()); + if (!source.exists()) { + throw new IOException(source.getRemote() + " does not exist."); + } else if (!source.isDirectory()) { + throw new IOException(source.getRemote() + " is not a directory."); + } + } + FilePath destination = ws.child(step.getFile()); + if (destination.exists() && !step.isOverwrite()) { + throw new IOException(destination.getRemote() + " exists."); + } + + logger.print("Compress " + source.getRemote()); + if (!StringUtils.isBlank(step.getGlob()) || !StringUtils.isBlank(step.getExclude())) { + logger.print(" filtered by [" + step.getGlob() + "] - [" + step.getExclude() + "]"); + } + logger.println(" to " + destination.getRemote()); + + callable.setDestination(destination); + Integer count = source.act(callable); + logger.println("Compressed " + count + " entries."); + + if (step.isArchive()) { + Run build = getContext().get(Run.class); + if (build == null) { + throw new MissingContextVariableException(Run.class); + } + Launcher launcher = getContext().get(Launcher.class); + if (launcher == null) { + throw new MissingContextVariableException(Launcher.class); + } + logger.println("Archiving " + destination.getRemote()); + + Map files = new HashMap<>(); + String s = step.getFile().replace('\\', '/'); + files.put(s, s); + build.pickArtifactManager().archive(ws, launcher, new BuildListenerAdapter(listener), files); + } + + return null; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/DecompressStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/DecompressStepExecution.java new file mode 100644 index 00000000..a6308148 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/DecompressStepExecution.java @@ -0,0 +1,92 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 CloudBees Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.jenkinsci.plugins.pipeline.utility.steps; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.FilePath; +import hudson.model.TaskListener; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; + +import java.io.IOException; + +/** + * Execution of {@link AbstractFileCompressStep}. + * + * @author Robert Sandell <rsandell@cloudbees.com>. + */ +public abstract class DecompressStepExecution extends SynchronousNonBlockingStepExecution { + private transient AbstractFileCallable callable; + private transient final AbstractFileDecompressStep step; + + protected DecompressStepExecution(@NonNull AbstractFileDecompressStep step, @NonNull StepContext context) { + super(context); + this.step = step; + } + + protected void setCallable(final AbstractFileCallable callable) { + this.callable = callable; + } + + @Override + protected Object run() throws IOException, InterruptedException { + TaskListener listener = getContext().get(TaskListener.class); + assert listener != null; + + FilePath workspace = getContext().get(FilePath.class); + assert workspace != null; + + if (step.isTest()) { + return test(listener, workspace); + } + + FilePath source = workspace.child(step.getFile()); + if (!source.exists()) { + throw new IOException(source.getRemote() + " does not exist."); + } else if (source.isDirectory()) { + throw new IOException(source.getRemote() + " is a directory."); + } + FilePath destination = workspace; + if (!StringUtils.isBlank(step.getDir())) { + destination = workspace.child(step.getDir()); + } + + callable.setDestination(destination); + return source.act(callable); + } + + private Object test(TaskListener listener, FilePath workspace) throws IOException, InterruptedException { + FilePath source = workspace.child(step.getFile()); + if (!source.exists()) { + listener.error(source.getRemote() + " does not exist."); + return Boolean.FALSE; + } else if (source.isDirectory()) { + listener.error(source.getRemote() + " is a directory."); + return Boolean.FALSE; + } + return source.act(callable); + } +} diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/conf/mf/ReadManifestStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/conf/mf/ReadManifestStepExecution.java index d976bf07..7e2f115d 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/conf/mf/ReadManifestStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/conf/mf/ReadManifestStepExecution.java @@ -29,7 +29,7 @@ import hudson.FilePath; import hudson.model.TaskListener; import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileOrTextStepExecution; -import org.jenkinsci.plugins.pipeline.utility.steps.zip.UnZipStepExecution; +import org.jenkinsci.plugins.pipeline.utility.steps.zip.UnZipStepExecution.UnZipFileCallable; import org.jenkinsci.plugins.workflow.steps.StepContext; import java.io.ByteArrayInputStream; @@ -85,7 +85,9 @@ private SimpleManifest parseFile(String file) throws IOException, InterruptedExc } String lcName = path.getName().toLowerCase(); if(lcName.endsWith(".jar") || lcName.endsWith(".war") || lcName.endsWith(".ear")) { - Map mf = path.act(new UnZipStepExecution.UnZipFileCallable(listener, ws, "META-INF/MANIFEST.MF", true, "UTF-8", false)); + UnZipFileCallable callable = new UnZipFileCallable(listener, "META-INF/MANIFEST.MF", true, "UTF-8", false); + callable.setDestination(ws); + Map mf = path.act(callable); String text = mf.get("META-INF/MANIFEST.MF"); if (isBlank(text)) { throw new FileNotFoundException(path.getRemote() + " does not seem to contain a manifest."); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java index 48eb8964..3e532a29 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStep.java @@ -82,10 +82,6 @@ public StepExecution start(StepContext context) throws Exception { @Extension public static class DescriptorImpl extends StepDescriptor { - public DescriptorImpl() { - - } - @Override public Set> getRequiredContext() { return ImmutableSet.of(TaskListener.class, FilePath.class); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java index 75ddd16a..aca1d059 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepExecution.java @@ -25,104 +25,54 @@ package org.jenkinsci.plugins.pipeline.utility.steps.tar; import edu.umd.cs.findbugs.annotations.NonNull; -import hudson.FilePath; -import hudson.Launcher; import hudson.Util; -import hudson.model.Run; -import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; import hudson.util.io.Archiver; import hudson.util.io.ArchiverFactory; -import jenkins.MasterToSlaveFileCallable; -import jenkins.util.BuildListenerAdapter; import org.apache.commons.lang.StringUtils; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; -import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileCallable; +import org.jenkinsci.plugins.pipeline.utility.steps.CompressStepExecution; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; /** * Execution of {@link TarStep}. * * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. */ -public class TarStepExecution extends SynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; +public class TarStepExecution extends CompressStepExecution { + private static final long serialVersionUID = -2607583480983180160L; private transient TarStep step; protected TarStepExecution(@NonNull TarStep step, @NonNull StepContext context) { - super(context); + super(step, context); this.step = step; } @Override protected Void run() throws Exception { - TaskListener listener = getContext().get(TaskListener.class); - assert listener != null; - FilePath ws = getContext().get(FilePath.class); - assert ws != null; - FilePath source = ws; - if (!StringUtils.isBlank(step.getDir())) { - source = ws.child(step.getDir()); - if (!source.exists()) { - throw new IOException(source.getRemote() + " does not exist."); - } else if (!source.isDirectory()) { - throw new IOException(source.getRemote() + " is not a directory."); - } - } - FilePath destination = ws.child(step.getFile()); - if (destination.exists() && !step.isOverwrite()) { - throw new IOException(destination.getRemote() + " exists."); - } - if (StringUtils.isBlank(step.getGlob()) && StringUtils.isBlank(step.getExclude())) { - listener.getLogger().println("Writing tar file of " + source.getRemote() + " to " + destination.getRemote()); - } else { - listener.getLogger().println("Writing tar file of " + source.getRemote() - + " filtered by [" + step.getGlob() + "] - [" + step.getExclude() + "] to " + destination.getRemote()); - } - int count = source.act(new TarItFileCallable(destination, step.getGlob(), step.getExclude(), step.isCompress(), step.isOverwrite())); - listener.getLogger().println("Tared " + count + " entries."); - if (step.isArchive()) { - Run build = getContext().get(Run.class); - if (build == null) { - throw new MissingContextVariableException(Run.class); - } - Launcher launcher = getContext().get(Launcher.class); - if (launcher == null) { - throw new MissingContextVariableException(Launcher.class); - } - listener.getLogger().println("Archiving " + destination.getRemote()); - Map files = new HashMap<>(); - String s = step.getFile().replace('\\', '/'); - files.put(s, s); - build.pickArtifactManager().archive(ws, launcher, new BuildListenerAdapter(listener), files); - } - - return null; + setCallable(new TarItFileCallable(step.getGlob(), step.getExclude(), step.isCompress(), step.isOverwrite())); + return super.run(); } /** * Performs the actual tar operation on the slave where the source dir is located. */ - static class TarItFileCallable extends MasterToSlaveFileCallable { - final FilePath tarFile; + static class TarItFileCallable extends AbstractFileCallable { final String glob; final String exclude; final boolean compress; final boolean overwrite; - public TarItFileCallable(FilePath tarFile, String glob, String exclude, boolean compress, boolean overwrite) { - this.tarFile = tarFile; + public TarItFileCallable(String glob, String exclude, boolean compress, boolean overwrite) { this.glob = StringUtils.isBlank(glob) ? "**/*" : glob; this.exclude = exclude; this.compress = compress; @@ -131,12 +81,12 @@ public TarItFileCallable(FilePath tarFile, String glob, String exclude, boolean @Override public Integer invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - Path p = Paths.get(tarFile.getRemote()); + Path p = Paths.get(getDestination().getRemote()); if (overwrite && Files.exists(p)) { Files.delete(p); //Will throw exception if it fails to delete it } - Archiver archiver = (compress ? ArchiverFactory.TARGZ : ArchiverFactory.TAR).create(tarFile.write()); + Archiver archiver = (compress ? ArchiverFactory.TARGZ : ArchiverFactory.TAR).create(getDestination().write()); FileSet fileSet = Util.createFileSet(dir, glob, exclude); DirectoryScanner scanner = fileSet.getDirectoryScanner(new org.apache.tools.ant.Project()); try { diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java index 274e50cd..a9f0e99a 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStep.java @@ -31,7 +31,7 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileDeCompressStep; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileDecompressStep; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; @@ -45,7 +45,7 @@ * * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. */ -public class UnTarStep extends AbstractFileDeCompressStep { +public class UnTarStep extends AbstractFileDecompressStep { @DataBoundConstructor public UnTarStep(String file) throws Descriptor.FormException { @@ -63,10 +63,6 @@ public StepExecution start(StepContext context) throws Exception { @Extension public static class DescriptorImpl extends StepDescriptor { - public DescriptorImpl() { - - } - @Override public Set> getRequiredContext() { return ImmutableSet.of(TaskListener.class, FilePath.class); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java index f82db7a1..f267c045 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/UnTarStepExecution.java @@ -28,15 +28,15 @@ import hudson.FilePath; import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; -import jenkins.MasterToSlaveFileCallable; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.tools.ant.types.selectors.SelectorUtils; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileCallable; +import org.jenkinsci.plugins.pipeline.utility.steps.DecompressStepExecution; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import java.io.File; import java.io.FileInputStream; @@ -51,67 +51,39 @@ * * @author Alexander Falkenstern <Alexander.Falkenstern@gmail.com>. */ -public class UnTarStepExecution extends SynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; +public class UnTarStepExecution extends DecompressStepExecution { + private static final long serialVersionUID = -7225291403337927553L; private transient UnTarStep step; protected UnTarStepExecution(@NonNull UnTarStep step, @NonNull StepContext context) { - super(context); + super(step, context); this.step = step; } @Override - protected Object run() throws Exception { + protected Object run() throws IOException, InterruptedException { TaskListener listener = getContext().get(TaskListener.class); assert listener != null; - FilePath ws = getContext().get(FilePath.class); - assert ws != null; - if (step.isTest()) { - return test(); - } - FilePath source = ws.child(step.getFile()); - if (!source.exists()) { - throw new IOException(source.getRemote() + " does not exist."); - } else if (source.isDirectory()) { - throw new IOException(source.getRemote() + " is a directory."); - } - FilePath destination = ws; - if (!StringUtils.isBlank(step.getDir())) { - destination = ws.child(step.getDir()); - } - return source.act(new UnTarFileCallable(listener, destination, step.getGlob(), step.isQuiet())); - } - - private Boolean test() throws IOException, InterruptedException { - TaskListener listener = getContext().get(TaskListener.class); - assert listener != null; - FilePath ws = getContext().get(FilePath.class); - assert ws != null; - FilePath source = ws.child(step.getFile()); - if (!source.exists()) { - listener.error(source.getRemote() + " does not exist."); - return false; - } else if (source.isDirectory()) { - listener.error(source.getRemote() + " is a directory."); - return false; + if (step.isTest()) { + setCallable(new TestTarFileCallable(listener)); + } else { + setCallable(new UnTarFileCallable(listener, step.getGlob(), step.isQuiet())); } - return source.act(new TestTarFileCallable(listener)); + return super.run(); } /** * Performs the untar on the slave where the tar file is located. */ - public static class UnTarFileCallable extends MasterToSlaveFileCallable { + public static class UnTarFileCallable extends AbstractFileCallable { private final TaskListener listener; - private final FilePath destination; private final String glob; private final boolean quiet; - public UnTarFileCallable(TaskListener listener, FilePath destination, String glob, boolean quiet) { + public UnTarFileCallable(TaskListener listener, String glob, boolean quiet) { this.listener = listener; - this.destination = destination; this.glob = glob; this.quiet = quiet; } @@ -130,7 +102,7 @@ public Void invoke(File tarFile, VirtualChannel channel) throws IOException, Int // Eat exception, may be not compressed file } - destination.mkdirs(); + getDestination().mkdirs(); try (TarArchiveInputStream tarStream = new TarArchiveInputStream(fileStream)) { logger.println("Extracting from " + tarFile.getAbsolutePath()); TarArchiveEntry entry; @@ -140,7 +112,7 @@ public Void invoke(File tarFile, VirtualChannel channel) throws IOException, Int continue; } - FilePath f = destination.child(entry.getName()); + FilePath f = getDestination().child(entry.getName()); if (entry.isDirectory()) { f.mkdirs(); } else { @@ -176,7 +148,7 @@ boolean matches(String path, String glob) { /** * Performs a test of a tar file on the slave where the file is. */ - static class TestTarFileCallable extends MasterToSlaveFileCallable { + static class TestTarFileCallable extends AbstractFileCallable { private TaskListener listener; public TestTarFileCallable(TaskListener listener) { diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java index 0593462d..d9bfc08a 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStep.java @@ -31,7 +31,7 @@ import hudson.model.Descriptor; import hudson.model.TaskListener; import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileDeCompressStep; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileDecompressStep; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; import org.jenkinsci.plugins.workflow.steps.StepExecution; @@ -46,7 +46,7 @@ * * @author Robert Sandell <rsandell@cloudbees.com>. */ -public class UnZipStep extends AbstractFileDeCompressStep { +public class UnZipStep extends AbstractFileDecompressStep { private String charset; private boolean read = false; @@ -125,10 +125,6 @@ public StepExecution start(StepContext context) throws Exception { @Extension public static class DescriptorImpl extends StepDescriptor { - public DescriptorImpl() { - - } - @Override public Set> getRequiredContext() { return ImmutableSet.of(TaskListener.class, FilePath.class); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java index 013fc658..c51f2bbe 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/UnZipStepExecution.java @@ -28,12 +28,12 @@ import hudson.FilePath; import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; -import jenkins.MasterToSlaveFileCallable; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.tools.ant.types.selectors.SelectorUtils; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileCallable; +import org.jenkinsci.plugins.pipeline.utility.steps.DecompressStepExecution; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import java.io.File; import java.io.IOException; @@ -55,68 +55,41 @@ * * @author Robert Sandell <rsandell@cloudbees.com>. */ -public class UnZipStepExecution extends SynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; +public class UnZipStepExecution extends DecompressStepExecution { + private static final long serialVersionUID = 6445244612862545236L; private transient UnZipStep step; protected UnZipStepExecution(@NonNull UnZipStep step, @NonNull StepContext context) { - super(context); + super(step, context); this.step = step; } @Override - protected Object run() throws Exception { + protected Object run() throws IOException, InterruptedException { TaskListener listener = getContext().get(TaskListener.class); assert listener != null; - FilePath ws = getContext().get(FilePath.class); - assert ws != null; - if (step.isTest()) { - return test(); - } - FilePath source = ws.child(step.getFile()); - if (!source.exists()) { - throw new IOException(source.getRemote() + " does not exist."); - } else if (source.isDirectory()) { - throw new IOException(source.getRemote() + " is a directory."); - } - FilePath destination = ws; - if (!StringUtils.isBlank(step.getDir())) { - destination = ws.child(step.getDir()); - } - return source.act(new UnZipFileCallable(listener, destination, step.getGlob(), step.isRead(),step.getCharset(),step.isQuiet())); - } - private Boolean test() throws IOException, InterruptedException { - TaskListener listener = getContext().get(TaskListener.class); - assert listener != null; - FilePath ws = getContext().get(FilePath.class); - assert ws != null; - FilePath source = ws.child(step.getFile()); - if (!source.exists()) { - listener.error(source.getRemote() + " does not exist."); - return false; - } else if (source.isDirectory()) { - listener.error(source.getRemote() + " is a directory."); - return false; + if (step.isTest()) { + setCallable(new TestZipFileCallable(listener)); + } else { + setCallable(new UnZipFileCallable(listener, step.getGlob(), step.isRead(),step.getCharset(),step.isQuiet())); } - return source.act(new TestZipFileCallable(listener)); + return super.run(); } /** * Performs the unzip on the slave where the zip file is located. */ - public static class UnZipFileCallable extends MasterToSlaveFileCallable> { + public static class UnZipFileCallable extends AbstractFileCallable> { private final TaskListener listener; - private final FilePath destination; private final String glob; private final boolean read; private final boolean quiet; private final String charset; - public UnZipFileCallable(TaskListener listener, FilePath destination, String glob, boolean read, String charset, boolean quiet) { + public UnZipFileCallable(TaskListener listener, String glob, boolean read, String charset, boolean quiet) { this.listener = listener; - this.destination = destination; this.glob = glob; this.read = read; this.charset = charset; @@ -126,7 +99,7 @@ public UnZipFileCallable(TaskListener listener, FilePath destination, String glo @Override public Map invoke(File zipFile, VirtualChannel channel) throws IOException, InterruptedException { if (!read) { - destination.mkdirs(); + getDestination().mkdirs(); } PrintStream logger = listener.getLogger(); boolean doGlob = !StringUtils.isBlank(glob); @@ -140,7 +113,7 @@ public Map invoke(File zipFile, VirtualChannel channel) throws I if (doGlob && !matches(entry.getName(), glob)) { continue; } - FilePath f = destination.child(entry.getName()); + FilePath f = getDestination().child(entry.getName()); if (entry.isDirectory()) { if (!read) { f.mkdirs(); @@ -196,7 +169,7 @@ boolean matches(String path, String glob) { /** * Performs a test of a zip file on the slave where the file is. */ - static class TestZipFileCallable extends MasterToSlaveFileCallable { + static class TestZipFileCallable extends AbstractFileCallable { private TaskListener listener; public TestZipFileCallable(TaskListener listener) { diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java index 2b5ff563..c0059c4f 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStep.java @@ -69,10 +69,6 @@ public StepExecution start(StepContext context) throws Exception { @Extension public static class DescriptorImpl extends StepDescriptor { - public DescriptorImpl() { - - } - @Override public Set> getRequiredContext() { return ImmutableSet.of(TaskListener.class, FilePath.class); diff --git a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java index 2917d4ac..d2d5200c 100644 --- a/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepExecution.java @@ -26,89 +26,42 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.FilePath; -import hudson.Launcher; import hudson.Util; -import hudson.model.Run; -import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; import hudson.util.io.Archiver; import hudson.util.io.ArchiverFactory; -import jenkins.MasterToSlaveFileCallable; -import jenkins.util.BuildListenerAdapter; import org.apache.commons.lang.StringUtils; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; -import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException; +import org.jenkinsci.plugins.pipeline.utility.steps.AbstractFileCallable; +import org.jenkinsci.plugins.pipeline.utility.steps.CompressStepExecution; import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; /** * Execution of {@link ZipStep}. * * @author Robert Sandell <rsandell@cloudbees.com>. */ -public class ZipStepExecution extends SynchronousNonBlockingStepExecution { - private static final long serialVersionUID = 1L; +public class ZipStepExecution extends CompressStepExecution { + private static final long serialVersionUID = 3738228158324163075L; private transient ZipStep step; protected ZipStepExecution(@NonNull ZipStep step, @NonNull StepContext context) { - super(context); + super(step, context); this.step = step; } @Override protected Void run() throws Exception { - TaskListener listener = getContext().get(TaskListener.class); - assert listener != null; - FilePath ws = getContext().get(FilePath.class); - assert ws != null; - FilePath source = ws; - if (!StringUtils.isBlank(step.getDir())) { - source = ws.child(step.getDir()); - if (!source.exists()) { - throw new IOException(source.getRemote() + " does not exist."); - } else if (!source.isDirectory()) { - throw new IOException(source.getRemote() + " is not a directory."); - } - } - FilePath destination = ws.child(step.getFile()); - if (destination.exists() && !step.isOverwrite()) { - throw new IOException(destination.getRemote() + " exists."); - } - if (StringUtils.isBlank(step.getGlob()) && StringUtils.isBlank(step.getExclude())) { - listener.getLogger().println("Writing zip file of " + source.getRemote() + " to " + destination.getRemote()); - } else { - listener.getLogger().println("Writing zip file of " + source.getRemote() - + " filtered by [" + step.getGlob() + "] - [" + step.getExclude() + "] to " + destination.getRemote()); - } - int count = source.act(new ZipItFileCallable(destination, step.getGlob(), step.getExclude(), step.isOverwrite())); - listener.getLogger().println("Zipped " + count + " entries."); - if (step.isArchive()) { - Run build = getContext().get(Run.class); - if (build == null) { - throw new MissingContextVariableException(Run.class); - } - Launcher launcher = getContext().get(Launcher.class); - if (launcher == null) { - throw new MissingContextVariableException(Launcher.class); - } - listener.getLogger().println("Archiving " + destination.getRemote()); - Map files = new HashMap<>(); - String s = step.getFile().replace('\\', '/'); - files.put(s, s); - build.pickArtifactManager().archive(ws, launcher, new BuildListenerAdapter(listener), files); - } - - return null; + setCallable(new ZipItFileCallable(step.getGlob(), step.getExclude(), step.isOverwrite())); + return super.run(); } /** @@ -117,14 +70,12 @@ protected Void run() throws Exception { * This is a more direct implementation because {@link FilePath#zip(FilePath)} * will include the source dir as a base path in the zip file while this implementation doesn't. */ - static class ZipItFileCallable extends MasterToSlaveFileCallable { - final FilePath zipFile; + static class ZipItFileCallable extends AbstractFileCallable { final String glob; final String exclude; final boolean overwrite; - public ZipItFileCallable(FilePath zipFile, String glob, String exclude, boolean overwrite) { - this.zipFile = zipFile; + public ZipItFileCallable(String glob, String exclude, boolean overwrite) { this.glob = StringUtils.isBlank(glob) ? "**/*" : glob; this.exclude = exclude; this.overwrite = overwrite; @@ -132,12 +83,12 @@ public ZipItFileCallable(FilePath zipFile, String glob, String exclude, boolean @Override public Integer invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - Path p = Paths.get(zipFile.getRemote()); + Path p = Paths.get(getDestination().getRemote()); if (overwrite && Files.exists(p)) { Files.delete(p); //Will throw exception if it fails to delete it } - Archiver archiver = ArchiverFactory.ZIP.create(zipFile.write()); + Archiver archiver = ArchiverFactory.ZIP.create(getDestination().write()); FileSet fs = Util.createFileSet(dir, glob, exclude); DirectoryScanner scanner = fs.getDirectoryScanner(new org.apache.tools.ant.Project()); try { diff --git a/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java index e5a4a3d8..9f59babb 100644 --- a/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/tar/TarStepTest.java @@ -85,7 +85,7 @@ public void simpleArchivedTar() throws Exception { " tar file: 'hello.tar', dir: 'hello', archive: true, compress: false\n" + "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing tar file", run); + j.assertLogContains("Compress", run); j.assertLogContains("Archiving", run); verifyArchivedHello(run, ""); } @@ -100,7 +100,7 @@ public void shouldNotPutOutputArchiveIntoItself() throws Exception { "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing tar file", run); + j.assertLogContains("Compress", run); verifyArchivedNotContainingItself(run); } @@ -115,7 +115,7 @@ public void shouldNotPutOutputArchiveIntoItself_nonCanonicalPath() throws Except " tar file: 'src/../src/output.tgz', dir: '', glob: '', archive: true\n" + "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing tar file", run); + j.assertLogContains("Compress", run); verifyArchivedNotContainingItself(run); } @@ -136,7 +136,7 @@ public void canArchiveFileWithSameName() throws Exception { WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing tar file", run); + j.assertLogContains("Compress", run); assertTrue("Build should have artifacts", run.getHasArtifacts()); Run.Artifact artifact = run.getArtifacts().get(0); assertEquals("output.tgz", artifact.getFileName()); @@ -184,7 +184,6 @@ public void excludedPatternWithAll() throws Exception { "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); verifyArchivedHello(run, ""); - } @Test @@ -248,7 +247,7 @@ public void noExistingTarFileWithOverwrite() throws Exception { WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0).get()); j.assertLogNotContains("java.io.IOException", run); j.assertLogNotContains("Failed to delete", run); - j.assertLogContains("Tared 1 entries.", run); + j.assertLogContains("Compressed 1 entries.", run); } private void verifyArchivedHello(WorkflowRun run, String basePath) throws IOException { diff --git a/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepTest.java b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepTest.java index 18ddd124..781753d3 100644 --- a/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/pipeline/utility/steps/zip/ZipStepTest.java @@ -85,7 +85,7 @@ public void simpleArchivedZip() throws Exception { " zip zipFile: 'hello.zip', dir: 'hello', archive: true\n" + "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing zip file", run); + j.assertLogContains("Compress", run); j.assertLogContains("Archiving", run); verifyArchivedHello(run, ""); @@ -102,7 +102,7 @@ public void shouldNotPutOutputArchiveIntoItself() throws Exception { "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing zip file", run); + j.assertLogContains("Compress", run); verifyArchivedNotContainingItself(run); } @@ -119,7 +119,7 @@ public void shouldNotPutOutputArchiveIntoItself_nonCanonicalPath() throws Except " zip zipFile: 'src/../src/output.zip', dir: '', glob: '', archive: true\n" + "}", true)); WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing zip file", run); + j.assertLogContains("Compress", run); verifyArchivedNotContainingItself(run); } @@ -140,7 +140,7 @@ public void canArchiveFileWithSameName() throws Exception { WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); run = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - j.assertLogContains("Writing zip file", run); + j.assertLogContains("Compress", run); assertTrue("Build should have artifacts", run.getHasArtifacts()); Run.Artifact artifact = run.getArtifacts().get(0); assertEquals("output.zip", artifact.getFileName()); @@ -261,7 +261,7 @@ public void noExistingZipFileWithOverwrite() throws Exception { WorkflowRun run = j.assertBuildStatusSuccess(p.scheduleBuild2(0).get()); j.assertLogNotContains("java.io.IOException", run); j.assertLogNotContains("Failed to delete", run); - j.assertLogContains("Zipped 1 entries.", run); + j.assertLogContains("Compressed 1 entries.", run); } private void verifyArchivedHello(WorkflowRun run, String basePath) throws IOException {