| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.googlesource.gerrit.plugins.deleteproject.fs; |
| |
| import com.google.gerrit.extensions.annotations.PluginName; |
| import com.google.gerrit.extensions.api.changes.NotifyHandling; |
| import com.google.gerrit.extensions.events.ProjectDeletedListener; |
| import com.google.gerrit.extensions.registration.DynamicSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.server.config.PluginConfigFactory; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.project.ProjectResource; |
| import com.google.inject.Inject; |
| import com.googlesource.gerrit.plugins.deleteproject.CannotDeleteProjectException; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.StandardCopyOption; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.RepositoryCache; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| public class FilesystemDeleteHandler { |
| private static final Logger log = LoggerFactory.getLogger(FilesystemDeleteHandler.class); |
| |
| private final GitRepositoryManager repoManager; |
| private final DynamicSet<ProjectDeletedListener> deletedListener; |
| private final PluginConfigFactory cfgFactory; |
| private final String pluginName; |
| |
| @Inject |
| public FilesystemDeleteHandler( |
| GitRepositoryManager repoManager, |
| DynamicSet<ProjectDeletedListener> deletedListener, |
| PluginConfigFactory cfgFactory, |
| @PluginName String pluginName) { |
| this.repoManager = repoManager; |
| this.deletedListener = deletedListener; |
| this.cfgFactory = cfgFactory; |
| this.pluginName = pluginName; |
| } |
| |
| public void delete(Project project, boolean preserveGitRepository) |
| throws IOException, RepositoryNotFoundException { |
| // Remove from the jgit cache |
| Repository repository = repoManager.openRepository(project.getNameKey()); |
| File repoFile = repository.getDirectory(); |
| cleanCache(repository); |
| if (!preserveGitRepository) { |
| deleteGitRepository(project.getNameKey(), repoFile); |
| } |
| } |
| |
| public void assertCanDelete(ProjectResource rsrc, boolean preserveGitRepository) |
| throws CannotDeleteProjectException { |
| if (!preserveGitRepository |
| && !cfgFactory |
| .getFromGerritConfig(pluginName) |
| .getBoolean("allowDeletionOfReposWithTags", true)) { |
| assertHasNoTags(rsrc); |
| } |
| } |
| |
| private void assertHasNoTags(ProjectResource rsrc) throws CannotDeleteProjectException { |
| try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) { |
| if (!repo.getRefDatabase().getRefs(Constants.R_TAGS).isEmpty()) { |
| throw new CannotDeleteProjectException( |
| String.format("Project %s has tags", rsrc.getName())); |
| } |
| } catch (IOException e) { |
| throw new CannotDeleteProjectException(e); |
| } |
| } |
| |
| private void deleteGitRepository(final Project.NameKey project, final File repoFile) |
| throws IOException { |
| // Delete the repository from disk |
| Path basePath = getBasePath(repoFile.toPath(), project); |
| Path trash = moveToTrash(repoFile.toPath(), basePath, project); |
| boolean ok = false; |
| try { |
| recursiveDelete(trash); |
| ok = true; |
| } catch (IOException e) { |
| // Only log if delete failed - repo already moved to trash. |
| // Otherwise, listeners are never called. |
| log.warn("Error trying to delete {}", trash, e); |
| } |
| |
| // Delete parent folders if they are (now) empty |
| if (ok) { |
| try { |
| recursiveDeleteParent(repoFile.getParentFile(), basePath.toFile()); |
| } catch (IOException e) { |
| log.warn("Couldn't delete (empty) parents of {}", repoFile, e); |
| } |
| } |
| |
| // Send an event that the repository was deleted |
| ProjectDeletedListener.Event event = |
| new ProjectDeletedListener.Event() { |
| @Override |
| public String getProjectName() { |
| return project.get(); |
| } |
| |
| @Override |
| public NotifyHandling getNotify() { |
| return NotifyHandling.NONE; |
| } |
| }; |
| for (ProjectDeletedListener l : deletedListener) { |
| try { |
| l.onProjectDeleted(event); |
| } catch (RuntimeException e) { |
| log.warn("Failure in ProjectDeletedListener", e); |
| } |
| } |
| } |
| |
| private Path getBasePath(Path repo, Project.NameKey project) { |
| Path projectPath = Paths.get(project.get()); |
| return repo.getRoot() |
| .resolve(repo.subpath(0, repo.getNameCount() - projectPath.getNameCount())); |
| } |
| |
| private Path moveToTrash(Path directory, Path basePath, Project.NameKey nameKey) |
| throws IOException { |
| Path trashRepo = |
| basePath.resolve(nameKey.get() + "." + System.currentTimeMillis() + ".%deleted%.git"); |
| return Files.move(directory, trashRepo, StandardCopyOption.ATOMIC_MOVE); |
| } |
| |
| private void cleanCache(final Repository repository) { |
| repository.close(); |
| RepositoryCache.close(repository); |
| } |
| |
| /** |
| * Recursively delete the specified file and all of its contents. |
| * |
| * @throws IOException |
| */ |
| private void recursiveDelete(Path file) throws IOException { |
| Files.walkFileTree( |
| file, |
| new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) |
| throws IOException { |
| Files.delete(file); |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { |
| if (e != null) { |
| throw e; |
| } |
| Files.delete(dir); |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } |
| |
| /** |
| * Recursively delete the specified file and its parent files until we hit the file {@code Until} |
| * or the parent file is populated. This is used when we have a tree structure such as a/b/c/d.git |
| * and a/b/e.git - if we delete a/b/c/d.git, we no longer need a/b/c/. |
| */ |
| private void recursiveDeleteParent(File file, File until) throws IOException { |
| if (file.equals(until)) { |
| return; |
| } |
| if (file.listFiles().length == 0) { |
| File parent = file.getParentFile(); |
| Files.delete(file.toPath()); |
| recursiveDeleteParent(parent, until); |
| } |
| } |
| } |