org.opensearch.gradle.tar.SymbolicLinkPreservingTar Maven / Gradle / Ivy
Show all versions of build-tools Show documentation
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.gradle.tar;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarConstants;
import org.apache.commons.compress.archivers.zip.UnixStat;
import org.gradle.api.GradleException;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.file.RegularFile;
import org.gradle.api.internal.file.CopyActionProcessingStreamAction;
import org.gradle.api.internal.file.archive.compression.ArchiveOutputStreamFactory;
import org.gradle.api.internal.file.archive.compression.Bzip2Archiver;
import org.gradle.api.internal.file.archive.compression.GzipArchiver;
import org.gradle.api.internal.file.archive.compression.SimpleCompressor;
import org.gradle.api.internal.file.copy.CopyAction;
import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.WorkResult;
import org.gradle.api.tasks.WorkResults;
import org.gradle.api.tasks.bundling.Tar;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;
/**
* A custom archive task that assembles a tar archive that preserves symbolic links.
*
* This task is necessary because the built-in task {@link org.gradle.api.tasks.bundling.Tar} does not preserve symbolic links.
*/
public class SymbolicLinkPreservingTar extends Tar {
private long lastModifiedTimestamp = 0;
public void setLastModifiedTimestamp(long lastModifiedTimestamp) {
this.lastModifiedTimestamp = lastModifiedTimestamp;
}
@Override
protected CopyAction createCopyAction() {
final ArchiveOutputStreamFactory compressor;
switch (getCompression()) {
case BZIP2:
compressor = Bzip2Archiver.getCompressor();
break;
case GZIP:
compressor = GzipArchiver.getCompressor();
break;
default:
compressor = new SimpleCompressor();
break;
}
return new SymbolicLinkPreservingTarCopyAction(getArchiveFile(), compressor, isPreserveFileTimestamps(), lastModifiedTimestamp);
}
private static class SymbolicLinkPreservingTarCopyAction implements CopyAction {
private final Provider tarFile;
private final ArchiveOutputStreamFactory compressor;
private final boolean isPreserveFileTimestamps;
private final long lastModifiedTimestamp;
SymbolicLinkPreservingTarCopyAction(
final Provider tarFile,
final ArchiveOutputStreamFactory compressor,
final boolean isPreserveFileTimestamps,
final long lastModifiedTimestamp
) {
this.tarFile = tarFile;
this.compressor = compressor;
this.isPreserveFileTimestamps = isPreserveFileTimestamps;
this.lastModifiedTimestamp = lastModifiedTimestamp;
}
@Override
public WorkResult execute(final CopyActionProcessingStream stream) {
try (
OutputStream out = compressor.createArchiveOutputStream(tarFile.get().getAsFile());
TarArchiveOutputStream tar = new TarArchiveOutputStream(out)
) {
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
stream.process(new SymbolicLinkPreservingTarStreamAction(tar));
} catch (final IOException e) {
throw new GradleException("failed writing tar file [" + tarFile + "]", e);
}
return WorkResults.didWork(true);
}
private class SymbolicLinkPreservingTarStreamAction implements CopyActionProcessingStreamAction {
private final TarArchiveOutputStream tar;
/*
* When Gradle walks the file tree, it will follow symbolic links. This means that if there is a symbolic link to a directory
* in the source file tree, we could otherwise end up duplicating the entries below that directory in the resulting tar archive.
* To avoid this, we track which symbolic links we have visited, and skip files that are children of symbolic links that we have
* already visited.
*/
private final Set visitedSymbolicLinks = new HashSet<>();
SymbolicLinkPreservingTarStreamAction(final TarArchiveOutputStream tar) {
this.tar = tar;
}
@Override
public void processFile(final FileCopyDetailsInternal details) {
if (isChildOfVisitedSymbolicLink(details) == false) {
if (isSymbolicLink(details)) {
visitSymbolicLink(details);
} else if (details.isDirectory()) {
visitDirectory(details);
} else {
visitFile(details);
}
}
}
private boolean isChildOfVisitedSymbolicLink(final FileCopyDetailsInternal details) {
final File file;
try {
file = details.getFile();
} catch (final UnsupportedOperationException e) {
// we get invoked with stubbed details, there is no way to introspect this other than catching this exception
return false;
}
for (final File symbolicLink : visitedSymbolicLinks) {
if (isChildOf(symbolicLink, file)) return true;
}
return false;
}
private boolean isChildOf(final File directory, final File file) {
return file.toPath().startsWith(directory.toPath());
}
private boolean isSymbolicLink(final FileCopyDetailsInternal details) {
final File file;
try {
file = details.getFile();
} catch (final UnsupportedOperationException e) {
// we get invoked with stubbed details, there is no way to introspect this other than catching this exception
return false;
}
return Files.isSymbolicLink(file.toPath());
}
private void visitSymbolicLink(final FileCopyDetailsInternal details) {
visitedSymbolicLinks.add(details.getFile());
final TarArchiveEntry entry = new TarArchiveEntry(details.getRelativePath().getPathString(), TarConstants.LF_SYMLINK);
entry.setModTime(getModTime(details));
entry.setMode(UnixStat.LINK_FLAG | details.getPermissions().toUnixNumeric());
try {
entry.setLinkName(Files.readSymbolicLink(details.getFile().toPath()).toString());
tar.putArchiveEntry(entry);
tar.closeArchiveEntry();
} catch (final IOException e) {
handleProcessingException(details, e);
}
}
private void visitDirectory(final FileCopyDetailsInternal details) {
final TarArchiveEntry entry = new TarArchiveEntry(details.getRelativePath().getPathString() + "/");
entry.setModTime(getModTime(details));
entry.setMode(UnixStat.DIR_FLAG | details.getPermissions().toUnixNumeric());
try {
tar.putArchiveEntry(entry);
tar.closeArchiveEntry();
} catch (final IOException e) {
handleProcessingException(details, e);
}
}
private void visitFile(final FileCopyDetailsInternal details) {
final TarArchiveEntry entry = new TarArchiveEntry(details.getRelativePath().getPathString());
entry.setModTime(getModTime(details));
entry.setMode(UnixStat.FILE_FLAG | details.getPermissions().toUnixNumeric());
entry.setSize(details.getSize());
try {
tar.putArchiveEntry(entry);
details.copyTo(tar);
tar.closeArchiveEntry();
} catch (final IOException e) {
handleProcessingException(details, e);
}
}
private void handleProcessingException(final FileCopyDetailsInternal details, final IOException e) {
throw new GradleException("could not add [" + details + "] to tar file [" + tarFile + "]", e);
}
}
private long getModTime(final FileCopyDetails details) {
return isPreserveFileTimestamps ? details.getLastModified() : lastModifiedTimestamp;
}
}
}