Skip to content
Permalink
Browse files

Merge pull request #7 from PierreBtz/JENKINS-43434

Take folders into account when shelving/unshelving
  • Loading branch information...
PierreBtz committed Jul 20, 2018
2 parents fa00a57 + 632586f commit 94e8e0193f81cf6f4ef4e8a1e2a6fef145bd74e5
@@ -50,6 +50,13 @@
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>

<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>cloudbees-folder</artifactId>
<version>6.5.1</version>
<optional>true</optional>
</dependency>
</dependencies>

<scm>
@@ -1,15 +1,19 @@
package org.jvnet.hudson.plugins.shelveproject;

import hudson.model.Hudson;
import hudson.model.Queue;
import jenkins.model.Jenkins;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;

import java.io.File;
import java.nio.file.Files;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.jvnet.hudson.plugins.shelveproject.ShelveProjectExecutable.ARCHIVE_FILE_EXTENSION;

public class DeleteProjectExecutable
implements Queue.Executable
{
@@ -30,32 +34,28 @@
return parentTask;
}

public void run()
{
for (String shelvedProjectArchiveName : shelvedProjectArchiveNames)
{
public void run() {
for (String shelvedProjectArchiveName : shelvedProjectArchiveNames) {
final File shelvedProjectArchive = getArchiveFile(shelvedProjectArchiveName);
LOGGER.info( "Deleting project [" + shelvedProjectArchiveName + "]." );
try
{
shelvedProjectArchive.delete();
}
catch ( Exception e )
{
LOGGER.log( Level.SEVERE, "Could not delete project archive [" + shelvedProjectArchiveName + "].", e );
LOGGER.info("Deleting project [" + shelvedProjectArchiveName + "].");
try {
if(ARCHIVE_FILE_EXTENSION.equals(FilenameUtils.getExtension(shelvedProjectArchiveName))) {
Files.delete(ShelvedProject.getMetadataFileFromArchive(shelvedProjectArchive));
}
Files.delete(shelvedProjectArchive.toPath());
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Could not delete project archive [" + shelvedProjectArchiveName + "].", e);
}
}
}

private File getArchiveFile(String shelvedProjectArchiveName) {
static File getArchiveFile(String shelvedProjectArchiveName) {
// JENKINS-8759 - The archive name comes from the html form, so take a bit extra care for security reasons by
// only accessing the archive if it exists in the directory of shevled projects.
File shelvedProjectsDirectory = new File( Hudson.getInstance().getRootDir(), "shelvedProjects" );
File shelvedProjectsDirectory = new File(Jenkins.getInstance().getRootDir(), ShelvedProjectsAction.SHELVED_PROJECTS_DIRECTORY);
Collection<File> files = FileUtils.listFiles(shelvedProjectsDirectory, null, false);
for (File file : files)
{
if (StringUtils.equals(file.getName(), shelvedProjectArchiveName))
{
for (File file : files) {
if (StringUtils.equals(file.getName(), shelvedProjectArchiveName)) {
return file;
}
}
@@ -1,46 +1,54 @@
package org.jvnet.hudson.plugins.shelveproject;

import com.cloudbees.hudson.plugins.folder.AbstractFolder;
import hudson.FilePath;
import hudson.model.AbstractProject;
import hudson.model.ItemGroup;
import hudson.model.Queue;
import jenkins.model.Jenkins;

import javax.annotation.Nonnull;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ShelveProjectExecutable
implements Queue.Executable
{
private final static Logger LOGGER = Logger.getLogger( ShelveProjectExecutable.class.getName() );
implements Queue.Executable {
private final static Logger LOGGER = Logger.getLogger(ShelveProjectExecutable.class.getName());

static final String METADATA_FILE_EXTENSION = "properties";
static final String ARCHIVE_FILE_EXTENSION = "tar";

static final String PROJECT_PATH_PROPERTY = "project.path";
static final String PROJECT_FULL_NAME_PROPERTY = "project.fullname";
static final String PROJECT_NAME_PROPERTY = "project.name";
static final String ARCHIVE_TIME_PROPERTY = "archive.time";

private final AbstractProject project;

private final Queue.Task parentTask;

public ShelveProjectExecutable( Queue.Task parentTask, AbstractProject project )
{
public ShelveProjectExecutable(Queue.Task parentTask, AbstractProject project) {
this.parentTask = parentTask;
this.project = project;
}

public Queue.Task getParent()
{
public Queue.Task getParent() {
return parentTask;
}

public void run()
{
if ( archiveProject() )
{
public void run() {
if (archiveProject()) {
deleteProject();
}
}

public long getEstimatedDuration()
{
public long getEstimatedDuration() {
return -1; // impossible to estimate duration
}

@@ -51,49 +59,104 @@ private boolean archiveProject() {
LOGGER.info("Creating archive for project [" + project.getName() + "].");
try {
Path projectRootPath = project.getRootDir().toPath();
FilePath sourcePath = new FilePath(projectRootPath.toFile());
List<String> regexp = createListOfFoldersToBackup();
regexp.add(relativizeToJenkinsJobsDirectory(projectRootPath) + "/**/*");
long archiveTime = System.currentTimeMillis();
String backupBaseName = project.getName() + "-" + archiveTime;
Path rootPath = Jenkins.getInstance().getRootDir().toPath();
Path shelvedProjectRoot = rootPath.resolve(ShelvedProjectsAction.SHELVED_PROJECTS_DIRECTORY);
if(!Files.exists(shelvedProjectRoot)) {
if (!Files.exists(shelvedProjectRoot)) {
Files.createDirectory(shelvedProjectRoot);
}
Path archivePath = Files.createFile(shelvedProjectRoot.resolve(project.getName() + "-" + System.currentTimeMillis() + ".zip"));
buildMetadataFile(shelvedProjectRoot, backupBaseName, archiveTime);
// use a tar because of https://github.com/jenkinsci/jenkins/pull/2639 when using ant includes.
// and this will also fix https://issues.jenkins-ci.org/browse/JENKINS-10986.
// keeping the filename formatted as before, if external scripts depend on it they won't be broken
Path archivePath = Files.createFile(shelvedProjectRoot.resolve(backupBaseName + "." + ARCHIVE_FILE_EXTENSION));
FilePath destinationPath = new FilePath(archivePath.toFile());
sourcePath.zip(destinationPath);
tar(new FilePath(getJenkinsJobsDirectory()), destinationPath, String.join(",", regexp));
return true;
} catch (IOException | InterruptedException e) {
LOGGER.log(Level.SEVERE, "Could not archive project [" + project.getName() + "].", e);
return false;
}
}

private void wipeoutWorkspace()
{
LOGGER.info( "Wiping out workspace for project [" + project.getName() + "]." );
private void buildMetadataFile(Path shelvedProjectRoot, String backupBaseName, long archiveTime) throws IOException {
Path archivePath = Files.createFile(shelvedProjectRoot.resolve(backupBaseName + "." + METADATA_FILE_EXTENSION));
Path projectRootPath = project.getRootDir().toPath();
try (BufferedWriter writer = Files.newBufferedWriter(archivePath, Charset.forName("UTF-8"))) {
addNewProperty(writer, PROJECT_PATH_PROPERTY, escapeForPropertiesFile(relativizeToJenkinsJobsDirectory(projectRootPath)));
addNewProperty(writer, PROJECT_NAME_PROPERTY, project.getName());
addNewProperty(writer, ARCHIVE_TIME_PROPERTY, Long.toString(archiveTime));
addNewProperty(writer, PROJECT_FULL_NAME_PROPERTY, project.getFullName());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not write metadata for project [" + project.getName() + "].", e);
throw e;
}
}

private static String escapeForPropertiesFile(@Nonnull String path) {
// Windows is using \ while it's an escape character in properties files
return path.replaceAll("\\\\", java.util.regex.Matcher.quoteReplacement("\\\\"));
}

private static void addNewProperty(BufferedWriter writer, String key, String value) throws IOException {
writer.write(key + "=" + value);
writer.newLine();
}

// could probably be integrated in FilePath
private static void tar(FilePath origin, FilePath dst, String glob) throws IOException, InterruptedException {
try (OutputStream os = dst.write()) {
origin.tar(os, glob);
}
}

private List<String> createListOfFoldersToBackup() {
List<String> regexp = new ArrayList<>();
// TODO: test Folder plugin is here
ItemGroup parent = project.getParent();
// technically not using Folder plugin code, but in practice, the Folder plugin code should be there for this
// situation to occur.
while (parent instanceof AbstractFolder) {
LOGGER.log(Level.INFO, "Archiving parent folder: " + parent.getFullName());
Path absoluteFolderConfigPath = parent.getRootDir().toPath().resolve("config.xml");
regexp.add(relativizeToJenkinsJobsDirectory(absoluteFolderConfigPath));
parent = ((AbstractFolder) parent).getParent();
}
return regexp;
}

private String relativizeToJenkinsJobsDirectory(Path path) {
return getJenkinsJobsDirectory().toPath().relativize(path).toString();
}

private File getJenkinsJobsDirectory() {
return new File(Jenkins.getInstance().getRootDir(), "jobs");
}

private void wipeoutWorkspace() {
LOGGER.info("Wiping out workspace for project [" + project.getName() + "].");
try {
project.doDoWipeOutWorkspace();
} catch (Exception e) {
LOGGER.log( Level.SEVERE, "Could not wipeout workspace [" + project.getName() + "].", e );
LOGGER.log(Level.SEVERE, "Could not wipeout workspace [" + project.getName() + "].", e);
}

}

private void deleteProject()
{
LOGGER.info( "Deleting project [" + project.getName() + "]." );
try
{
project.delete();
}
catch ( Exception e )
{
LOGGER.log( Level.SEVERE, "Could not delete project [" + project.getName() + "].", e );
private void deleteProject() {
LOGGER.info("Deleting project [" + project.getName() + "].");
try {
project.delete();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Could not delete project [" + project.getName() + "].", e);
}
}

@Override
public String toString()
{
public String toString() {
return "Shelving " + project.getName();
}
}
@@ -1,9 +1,21 @@
package org.jvnet.hudson.plugins.shelveproject;

import org.apache.commons.io.FilenameUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.jvnet.hudson.plugins.shelveproject.ShelveProjectExecutable.*;

public class ShelvedProject
{
private final static Logger LOGGER = Logger.getLogger(ShelvedProject.class.getName());

private String projectName;

private File archive;
@@ -51,4 +63,34 @@ public void setFormatedDate( String formatedDate )
{
this.formatedDate = formatedDate;
}

static ShelvedProject createFromTar(File archive) {
ShelvedProject shelvedProject = new ShelvedProject();
try {
Properties shelveProperties = loadMetadata(archive);
shelvedProject.setTimestamp(Long.parseLong(shelveProperties.getProperty(ARCHIVE_TIME_PROPERTY)));
shelvedProject.setArchive(archive);
// TODO this will need some cleaning, outside of the scope of this dev
shelvedProject.setFormatedDate(ShelvedProjectsAction.formatDate(shelvedProject.getTimestamp()));
shelvedProject.setProjectName(shelveProperties.getProperty(PROJECT_FULL_NAME_PROPERTY));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not load the archive properly for " + archive, e);
}
return shelvedProject;
}

static Properties loadMetadata(File archive) throws IOException {
Path metadataPath = getMetadataFileFromArchive(archive);
Properties shelveProperties = new Properties();
try (BufferedReader reader = Files.newBufferedReader(metadataPath)) {
shelveProperties.load(reader);

}
return shelveProperties;
}

static Path getMetadataFileFromArchive(File archive) {
return Paths.get(FilenameUtils.getFullPath(archive.getAbsolutePath()),
FilenameUtils.getBaseName(archive.getAbsolutePath()) + "." + METADATA_FILE_EXTENSION);
}
}
@@ -28,6 +28,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.jvnet.hudson.plugins.shelveproject.ShelveProjectExecutable.*;

@ExportedBean(defaultVisibility = 999)
@Extension
public class ShelvedProjectsAction
@@ -67,12 +69,15 @@ public String getUrlName() {
if (!Files.exists(shelvedProjectRoot)) {
Files.createDirectory(shelvedProjectRoot);
}
PathMatcher zipMatcher = FileSystems.getDefault().getPathMatcher("glob:**/*.zip");
PathMatcher legacyMatcher = FileSystems.getDefault().getPathMatcher("glob:**/*.zip");
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*." + ARCHIVE_FILE_EXTENSION);
Files.walkFileTree(shelvedProjectRoot, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
super.visitFile(file, attrs);
if (zipMatcher.matches(file)) {
if (legacyMatcher.matches(file)) {
projects.add(getLegacyShelvedProjectFromArchive(file.toFile()));
} else if(matcher.matches(file)) {
projects.add(getShelvedProjectFromArchive(file.toFile()));
}
return FileVisitResult.CONTINUE;
@@ -99,7 +104,11 @@ public int compare(ShelvedProject project1, ShelvedProject project2)
});
}

private ShelvedProject getShelvedProjectFromArchive( File archive )
private ShelvedProject getShelvedProjectFromArchive(File archive) {
return ShelvedProject.createFromTar(archive);
}

private ShelvedProject getLegacyShelvedProjectFromArchive(File archive )
{
ShelvedProject shelvedProject = new ShelvedProject();
shelvedProject.setProjectName( StringUtils.substringBeforeLast( archive.getName(), "-" ) );
@@ -154,7 +163,8 @@ private HttpResponse deleteProject(StaplerRequest request) {
return createRedirectToShelvedProjectsPage();
}

public String formatDate( long timestamp )
// TODO this will need some cleaning, outside of the scope of this dev
public static String formatDate( long timestamp )
{
SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "EEE, d MMM yyyy HH:mm:ss Z" );
return simpleDateFormat.format( new Date( timestamp ) );
Oops, something went wrong.

0 comments on commit 94e8e01

Please sign in to comment.
You can’t perform that action at this time.