diff --git a/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/eelPathUtils.kt b/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/eelPathUtils.kt index b2809d8bdb18..5d1d15fab00f 100644 --- a/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/eelPathUtils.kt +++ b/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/eelPathUtils.kt @@ -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(LinkOption.NOFOLLOW_LINKS)?.let { remoteView -> @@ -1108,29 +1127,6 @@ object EelPathUtils { scope: CoroutineScope, ) { coroutineScope { - - val attrs = sourceRoot.asNioPath().fileAttributesView(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 = ArrayDeque() val targetQ: ArrayDeque = 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(LinkOption.NOFOLLOW_LINKS).readAttributes() + val targetAttrs = targetRoot.fileAttributesView(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 {