IJPL-201078 Copying absolute symlinks to remote Eel

GitOrigin-RevId: 0afb9b3c0c334c17541832038db67c120b9cfa66
This commit is contained in:
Dragoslav Tamindzija
2025-10-02 11:04:05 +00:00
committed by intellij-monorepo-bot
parent a795a03d49
commit e99eeb84c1
@@ -570,10 +570,16 @@ object EelPathUtils {
@RequiresBackgroundThread
@RequiresBlockingContext
@VisibleForTesting
fun walkingTransfer(sourceRoot: Path, targetRoot: Path, removeSource: Boolean, fileAttributesStrategy: FileTransferAttributesStrategy) {
fun walkingTransfer(
sourceRoot: Path,
targetRoot: Path,
removeSource: Boolean,
fileAttributesStrategy: FileTransferAttributesStrategy,
absoluteSymlinkHandler: IncrementalWalkingTransferAbsoluteSymlinkHandler? = null,
) {
if (Registry.`is`("ijent.incremental.walking.transfer") && !removeSource) {
runBlockingMaybeCancellable {
incrementalWalkingTransfer(sourceRoot, targetRoot, fileAttributesStrategy)
incrementalWalkingTransfer(sourceRoot, targetRoot, fileAttributesStrategy, absoluteSymlinkHandler)
}
return
}
@@ -698,7 +704,7 @@ object EelPathUtils {
/**
* Function only checks permissions, and it ignores the owner, group, sticky bit, gid, and uid.
* If FileTransferAttributesStrategy is RequirePosixPermissions, it will be checked if remote file permissions contain required permissions.
**/
**/
private fun arePermissionsEqual(fileAttributesStrategy: FileTransferAttributesStrategy, local: WalkDirectoryEntry.Permissions, remote: WalkDirectoryEntry.Permissions): Boolean {
return when (local) {
is WalkDirectoryEntryPosix.Permissions -> {
@@ -797,7 +803,7 @@ object EelPathUtils {
) : DiffOperation()
// If a file has the same local and remote path but different file type, the existing one is deleted and replaced with a correct one.
// ReplaceFile is additionally used in case of symlinks. The symlink target cannot be changed in place, thus it requires replacing.
// ReplaceFile is additionally used in the case of a relative symlink. The symlink target cannot be changed in place, thus it requires replacing.
// Always syncs permission, attributes and timestamps.
data class ReplaceFile(
val localFile: WalkDirectoryEntry,
@@ -812,6 +818,12 @@ object EelPathUtils {
val localFile: WalkDirectoryEntry,
val remoteFile: WalkDirectoryEntry,
) : DiffOperation()
// Absolute symlinks are left untouched, and it is up to the user-provided lambda to handle it
data class AbsoluteSymlink(
val sourceSymlink: WalkDirectoryEntry,
val remoteSymlink: WalkDirectoryEntry?,
) : DiffOperation()
}
/**
@@ -888,7 +900,10 @@ object EelPathUtils {
// if there is a file locally but not on the remote side - the file was created
if (localEntry != null && remoteEntry == null) {
if (localEntry.type !is WalkDirectoryEntry.Type.Other) {
if (localEntry.type is WalkDirectoryEntry.Type.Symlink.Absolute) {
emit(DiffOperation.AbsoluteSymlink(localEntry, null))
}
else if (localEntry.type !is WalkDirectoryEntry.Type.Other) {
emit(DiffOperation.Create(localEntry))
}
localEntry = null
@@ -956,7 +971,8 @@ object EelPathUtils {
}
}
is WalkDirectoryEntry.Type.Symlink.Absolute -> {
// TODO: IJPL-201078
emit(DiffOperation.AbsoluteSymlink(localEntry, remoteEntry))
opEmitted = true
}
is WalkDirectoryEntry.Type.Other -> {
// other file types have been handled prior to this when
@@ -982,7 +998,10 @@ object EelPathUtils {
}
// if the local path is in lower lexicographical order than the remote path, it means that the local file was created
else if (pathComparison < 0) {
if (localEntry !is WalkDirectoryEntry.Type.Symlink.Absolute) { // TODO: IJPL-201078
if (localEntry is WalkDirectoryEntry.Type.Symlink.Absolute) {
emit(DiffOperation.AbsoluteSymlink(localEntry, null))
}
else {
emit(DiffOperation.Create(localEntry))
}
localEntry = null
@@ -1056,7 +1075,7 @@ object EelPathUtils {
}
if (setAttributes) {
if (localEntry.permissions == null) {
if (localEntry.attributes == null) {
error("Attributes are supposed to be transferred, but were not yielded")
}
remoteEntry.fileAttributesViewOrNull<DosFileAttributeView>(LinkOption.NOFOLLOW_LINKS)?.let { remoteView ->
@@ -1108,29 +1127,6 @@ object EelPathUtils {
scope: CoroutineScope,
) {
coroutineScope {
val attrs = sourceRoot.asNioPath().fileAttributesView<BasicFileAttributeView>(LinkOption.NOFOLLOW_LINKS).readAttributes()
when {
attrs.isDirectory -> {
if (!targetRoot.asNioPath().isDirectory(LinkOption.NOFOLLOW_LINKS)) {
targetEelApi.fs.delete(targetRoot, true).getOrThrow()
Files.createDirectory(targetRoot.asNioPath())
}
}
attrs.isRegularFile -> {
if (!targetRoot.asNioPath().isRegularFile(LinkOption.NOFOLLOW_LINKS)) {
targetEelApi.fs.delete(targetRoot, true).getOrThrow()
Files.createFile(targetRoot.asNioPath())
}
}
attrs.isSymbolicLink -> {
if (!targetRoot.asNioPath().isSymbolicLink()) {
targetEelApi.fs.delete(targetRoot, true).getOrThrow()
Files.createSymbolicLink(targetRoot.asNioPath(), Path(""))
}
}
}
val sourceQ: ArrayDeque<EelPath> = ArrayDeque()
val targetQ: ArrayDeque<EelPath> = ArrayDeque()
sourceQ.add(sourceRoot)
@@ -1292,13 +1288,31 @@ object EelPathUtils {
}
}
/**
* Callback invoked for each absolute symlink encountered during transfer.
* The lambda can do whatever it wants: recreate the symlink, copy contents,
* ignore it, call [incrementalWalkingTransfer] again on it, or implement any custom logic.
*/
fun interface IncrementalWalkingTransferAbsoluteSymlinkHandler {
/**
* @param sourceSymlink Information about the source symlink
* @param targetEntry Directory entry that lives on the path where the source symlink should be. May or may not be a symlink.
*/
suspend fun handle(sourceSymlink: WalkDirectoryEntry, targetEntry: WalkDirectoryEntry?)
}
/**
* Supports transferring directories, files, and symlinks. On POSIX permissions and timestamps are transferred as well if indicated
* using [FileTransferAttributesStrategy]. Symlinks are transferred as is, and the target path does not have to be valid.
* using [FileTransferAttributesStrategy]. Relative symlinks are transferred as is, and the target path does not have to be valid.
* Permissions on symlinks are not transferred as they are ignored by on Unix-like systems.
*/
@RequiresBackgroundThread
private suspend fun incrementalWalkingTransfer(sourceRoot: Path, targetRoot: Path, fileAttributesStrategy: FileTransferAttributesStrategy) {
private suspend fun incrementalWalkingTransfer(
sourceRoot: Path,
targetRoot: Path,
fileAttributesStrategy: FileTransferAttributesStrategy,
absoluteSymlinkHandler: IncrementalWalkingTransferAbsoluteSymlinkHandler?,
) {
coroutineScope {
val targetRootEel = targetRoot.asEelPath()
val remoteDescriptor = targetRootEel.descriptor
@@ -1307,8 +1321,32 @@ object EelPathUtils {
val localOsFamily = localPathEel.descriptor.osFamily
val remoteOsFamily = targetRoot.getEelDescriptor().osFamily
withContext(Dispatchers.IO) {
directoryOnlySync(localPathEel, targetRootEel, localPathEel.descriptor.toEelApi(), remoteDescriptor.toEelApi(), false, this)
val sourceAttrs = sourceRoot.fileAttributesView<BasicFileAttributeView>(LinkOption.NOFOLLOW_LINKS).readAttributes()
val targetAttrs = targetRoot.fileAttributesView<BasicFileAttributeView>(LinkOption.NOFOLLOW_LINKS).readAttributes()
when {
sourceAttrs.isDirectory -> {
if (!targetAttrs.isDirectory) {
remoteEelApi.fs.delete(targetRootEel, true).getOrThrow()
Files.createDirectory(targetRoot)
}
withContext(Dispatchers.IO) {
directoryOnlySync(localPathEel, targetRootEel, localPathEel.descriptor.toEelApi(), remoteDescriptor.toEelApi(), false, this)
}
}
sourceAttrs.isRegularFile -> {
if (!targetAttrs.isRegularFile) {
remoteEelApi.fs.delete(targetRootEel, true).getOrThrow()
Files.createFile(targetRoot)
}
}
sourceAttrs.isSymbolicLink -> {
// if targetRoot is a directory, it should be deleted to prevent it from being traversed
if (targetAttrs.isDirectory) {
remoteEelApi.fs.delete(targetRootEel, true).getOrThrow()
// a placeholder file is created so that, in the case of an absolute symlink, the user lambda receives the expected target path (where the absolute symlink should be)
Files.createFile(targetRoot)
}
}
}
val walkDirectoryOptionsSource = WalkDirectoryOptionsBuilder(localPathEel)
@@ -1344,17 +1382,13 @@ object EelPathUtils {
try {
when (diffOp) {
is DiffOperation.Create, is DiffOperation.ReplaceFile -> {
if (diffOp is DiffOperation.ReplaceFile) {
val ker = diffOp.remoteFile.path.asNioPath()
Files.delete(ker)
}
if (diffOp is DiffOperation.ReplaceFile) Files.delete(diffOp.remoteFile.path.asNioPath())
val localFile: WalkDirectoryEntry
if (diffOp is DiffOperation.Create) {
localFile = diffOp.localFile
val localFile = if (diffOp is DiffOperation.Create) {
diffOp.localFile
}
else {
localFile = (diffOp as DiffOperation.ReplaceFile).localFile
(diffOp as DiffOperation.ReplaceFile).localFile
}
val localFileNioPath = localFile.path.asNioPath()
@@ -1385,11 +1419,11 @@ object EelPathUtils {
is WalkDirectoryEntry.Type.Symlink.Relative -> {
var symlinkTarget = (localFile.type as WalkDirectoryEntry.Type.Symlink.Relative).symlinkRelativePath
if (localOsFamily != remoteOsFamily) {
if (remoteOsFamily.isWindows) {
symlinkTarget = symlinkTarget.replace("/", "\\")
symlinkTarget = if (remoteOsFamily.isWindows) {
symlinkTarget.replace("/", "\\")
}
else {
symlinkTarget = symlinkTarget.replace("\\", "/")
symlinkTarget.replace("\\", "/")
}
}
Files.createSymbolicLink(remoteAbsolutePath, Path(symlinkTarget))
@@ -1398,7 +1432,7 @@ object EelPathUtils {
//setPermissionsAndAttributes(localFile, remoteAbsolutePath, fileAttributesStrategy, false, true, true)
}
is WalkDirectoryEntry.Type.Symlink.Absolute -> {
// TODO: IJPL-201078
error("unreachable, absolute symlink should exclusively be handled by the user provided lambda")
}
is WalkDirectoryEntry.Type.Other -> {
// NOTE: other file types not supported
@@ -1430,6 +1464,12 @@ object EelPathUtils {
setPermissionsAndAttributes(diffOp.localFile, diffOp.remoteFile.path.asNioPath(), remoteEelApi, fileAttributesStrategy, diffOp.updatePermissions, diffOp.updateAttributes, diffOp.updateTimestamps)
}
}
is DiffOperation.AbsoluteSymlink -> {
when (absoluteSymlinkHandler) {
null -> LOG.info("No absolute symlink handler provided for incremental walking transfer, skipping symlink: ${diffOp.sourceSymlink.path}")
else -> absoluteSymlinkHandler.handle(diffOp.sourceSymlink, diffOp.remoteSymlink)
}
}
}
}
finally {