Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/remove rename #42

Merged
merged 2 commits into from
Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions Sources/Citadel/SFTP/Client/SFTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,53 @@ public final class SFTPClient {

self.logger.debug("SFTP created directory \(path)")
}

/// Remove a file at the specified path on the SFTP server
public func remove(
at filePath: String
) async throws {
self.logger.info("SFTP requesting remove file at '\(filePath)'")

let _ = try await sendRequest(.remove(.init(
requestId: allocateRequestId(),
filename: filePath
)))

self.logger.debug("SFTP removed file at \(filePath)")
}

/// Remove a directory at the specified path on the SFTP server
public func rmdir(
at filePath: String
) async throws {
self.logger.info("SFTP requesting remove directory at '\(filePath)'")

let _ = try await sendRequest(.rmdir(.init(
requestId: allocateRequestId(),
filePath: filePath
)))

self.logger.debug("SFTP removed directory at \(filePath)")
}

/// Rename a file
public func rename(
at oldPath: String,
to newPath: String,
flags: UInt32 = 0
) async throws {
self.logger.info("SFTP requesting rename file at '\(oldPath)' to '\(newPath)'")

let _ = try await sendRequest(.rename(.init(
requestId: allocateRequestId(),
oldPath: oldPath,
newPath: newPath,
flags: flags
)))

self.logger.debug("SFTP renamed file at \(oldPath) to \(newPath)")
}

}

extension SSHClient {
Expand Down
4 changes: 3 additions & 1 deletion Sources/Citadel/SFTP/SFTPBasicEnums.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public enum SFTPMessageType: UInt8 {
case rmdir = 15
case realpath = 16
case stat = 17
case readlink = 18
case rename = 18
case readlink = 19
case symlink = 20

case status = 101
Expand Down Expand Up @@ -84,6 +85,7 @@ public enum SFTPMessageType: UInt8 {
case .rmdir: return "SSH_FXP_RMDIR"
case .realpath: return "SSH_FXP_REALPATH"
case .stat: return "SSH_FXP_STAT"
case .rename: return "SSH_FXP_RENAME"
case .readlink: return "SSH_FXP_READLINK"
case .symlink: return "SSH_FXP_SYMLINK"

Expand Down
46 changes: 40 additions & 6 deletions Sources/Citadel/SFTP/SFTPMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ enum SFTPRequest: CustomDebugStringConvertible {
case readdir(SFTPMessage.ReadDir)
case opendir(SFTPMessage.OpenDir)
case realpath(SFTPMessage.RealPath)

case remove(SFTPMessage.Remove)
case rmdir(SFTPMessage.RmDir)
case rename(SFTPMessage.Rename)

var requestId: UInt32 {
get {
switch self {
Expand All @@ -56,6 +59,12 @@ enum SFTPRequest: CustomDebugStringConvertible {
return message.requestId
case .realpath(let message):
return message.requestId
case .remove(let message):
return message.requestId
case .rmdir(let message):
return message.requestId
case .rename(let message):
return message.requestId
}
}
}
Expand All @@ -82,6 +91,12 @@ enum SFTPRequest: CustomDebugStringConvertible {
return .readdir(message)
case .realpath(let message):
return .realpath(message)
case .remove(let message):
return .remove(message)
case .rmdir(let message):
return .rmdir(message)
case .rename(let message):
return .rename(message)
}
}

Expand All @@ -97,6 +112,9 @@ enum SFTPRequest: CustomDebugStringConvertible {
case .readdir(let message): return message.debugDescription
case .opendir(let message): return message.debugDescription
case .realpath(let message): return message.debugDescription
case .remove(let message): return message.debugDescription
case .rmdir(let message): return message.debugDescription
case .rename(let message): return message.debugDescription
}
}
}
Expand Down Expand Up @@ -159,7 +177,7 @@ enum SFTPResponse {
self = .name(message)
case .attributes(let message):
self = .attributes(message)
case .realpath, .openFile, .fstat, .closeFile, .read, .write, .initialize, .version, .stat, .lstat, .rmdir, .opendir, .readdir, .remove, .fsetstat, .setstat, .symlink, .readlink:
case .realpath, .openFile, .fstat, .closeFile, .read, .write, .initialize, .version, .stat, .lstat, .rmdir, .opendir, .readdir, .remove, .fsetstat, .setstat, .symlink, .readlink, .rename:
return nil
}
}
Expand Down Expand Up @@ -318,7 +336,19 @@ public enum SFTPMessage {
public var debugDescription: String { "{\(self.requestId)}(\(self.path),\(self.attributes)" }
fileprivate var debugVariantWithoutLargeData: Self { self }
}


public struct Rename: SFTPMessageContent {
public static let id = SFTPMessageType.rename

public let requestId: UInt32
public var oldPath: String
public var newPath: String
public var flags: UInt32

public var debugDescription: String { "{\(self.requestId)}(\(self.oldPath),\(self.newPath),\(self.flags))" }
fileprivate var debugVariantWithoutLargeData: Self { self }
}

public struct Symlink: SFTPMessageContent {
public static let id = SFTPMessageType.symlink

Expand All @@ -331,7 +361,7 @@ public enum SFTPMessage {
}

public struct Readlink: SFTPMessageContent {
public static let id = SFTPMessageType.symlink
public static let id = SFTPMessageType.readlink

public let requestId: UInt32
public var path: String
Expand Down Expand Up @@ -524,6 +554,7 @@ public enum SFTPMessage {
case name(Name)
case attributes(Attributes)
case readdir(ReadDir)
case rename(Rename)

public var messageType: SFTPMessageType {
switch self {
Expand Down Expand Up @@ -551,7 +582,8 @@ public enum SFTPMessage {
.fsetstat(let message as SFTPMessageContent),
.setstat(let message as SFTPMessageContent),
.symlink(let message as SFTPMessageContent),
.readlink(let message as SFTPMessageContent):
.readlink(let message as SFTPMessageContent),
.rename(let message as SFTPMessageContent):
return message.id
}
}
Expand Down Expand Up @@ -582,7 +614,8 @@ public enum SFTPMessage {
.fsetstat(let message as SFTPMessageContent),
.setstat(let message as SFTPMessageContent),
.symlink(let message as SFTPMessageContent),
.readlink(let message as SFTPMessageContent):
.readlink(let message as SFTPMessageContent),
.rename(let message as SFTPMessageContent):
return "\(message.id)\(message.debugDescription)"
}
}
Expand Down Expand Up @@ -613,6 +646,7 @@ public enum SFTPMessage {
case .setstat(let message): return Self.setstat(message.debugVariantWithoutLargeData)
case .symlink(let message): return Self.symlink(message.debugVariantWithoutLargeData)
case .readlink(let message): return Self.readlink(message.debugVariantWithoutLargeData)
case .rename(let message): return Self.rename(message.debugVariantWithoutLargeData)
}
}

Expand Down
19 changes: 19 additions & 0 deletions Sources/Citadel/SFTP/SFTPMessageParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,25 @@ struct SFTPMessageParser: ByteToMessageDecoder {
attributes: attributes
)
)
case .rename:
guard
let requestId = payload.readInteger(as: UInt32.self),
let oldPath = payload.readSSHString(),
let newPath = payload.readSSHString(),
let flags = payload.readInteger(as: UInt32.self)
else {
throw SFTPError.invalidPayload(type: type)
}

message = .rename(
.init(
requestId: requestId,
oldPath: oldPath,
newPath: newPath,
flags: flags
)
)

case .readlink:
guard
let requestId = payload.readInteger(as: UInt32.self),
Expand Down
11 changes: 11 additions & 0 deletions Sources/Citadel/SFTP/SFTPSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,33 @@ final class SFTPMessageSerializer: MessageToByteEncoder {
out.writeSSHString(&fstat.handle)
case .remove(let remove):
out.writeInteger(SFTPMessage.Remove.id.rawValue)
out.writeInteger(remove.requestId)
out.writeSSHString(remove.filename)
case .fsetstat(var fsetstat):
out.writeInteger(SFTPMessage.FileSetStat.id.rawValue)
out.writeInteger(fsetstat.requestId)
out.writeSSHString(&fsetstat.handle)
out.writeSFTPFileAttributes(fsetstat.attributes)
case .setstat(let setstat):
out.writeInteger(SFTPMessage.SetStat.id.rawValue)
out.writeInteger(setstat.requestId)
out.writeSSHString(setstat.path)
out.writeSFTPFileAttributes(setstat.attributes)
case .symlink(let symlink):
out.writeInteger(SFTPMessage.Symlink.id.rawValue)
out.writeInteger(symlink.requestId)
out.writeSSHString(symlink.linkPath)
out.writeSSHString(symlink.targetPath)
case .readlink(let readlink):
out.writeInteger(SFTPMessage.Symlink.id.rawValue)
out.writeInteger(readlink.requestId)
out.writeSSHString(readlink.path)
case .rename(let rename):
out.writeInteger(SFTPMessage.Rename.id.rawValue)
out.writeInteger(rename.requestId)
out.writeSSHString(rename.oldPath)
out.writeSSHString(rename.newPath)
out.writeInteger(rename.flags)
}

let length = out.writerIndex - lengthIndex - 4
Expand Down
3 changes: 3 additions & 0 deletions Sources/Citadel/SFTP/Server/SFTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public protocol SFTPDelegate {

/// Reads the target of the symbolic link at the given path. This is equivalent to the `readlink()` system call.
func readSymlink(atPath path: String, context: SSHContext) async throws -> [SFTPPathComponent]

/// Renames a file
func rename(oldPath: String, newPath: String, flags: UInt32, context: SSHContext) async throws -> SFTPStatusCode
}

struct SFTPServerSubsystem {
Expand Down
28 changes: 27 additions & 1 deletion Sources/Citadel/SFTP/Server/SFTPServerInboundHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,31 @@ final class SFTPServerInboundHandler: ChannelInboundHandler {
)
}.flatMapErrorThrowing { _ in }
}


func rename(command: SFTPMessage.Rename, context:ChannelHandlerContext) {
let promise = context.eventLoop.makePromise(of: SFTPStatusCode.self)
promise.completeWithTask {
try await self.delegate.rename(
oldPath: command.oldPath,
newPath: command.newPath,
flags: command.flags,
context: self.makeContext()
)
}
_ = promise.futureResult.flatMap { status -> EventLoopFuture<Void> in
context.channel.writeAndFlush(
SFTPMessage.status(
SFTPMessage.Status(
requestId: command.requestId,
errorCode: status,
message: "",
languageTag: "EN"
)
)
)
}.flatMapErrorThrowing { _ in }
}

func readlink(command: SFTPMessage.Readlink, context:ChannelHandlerContext) {
let promise = context.eventLoop.makePromise(of: [SFTPPathComponent].self)
promise.completeWithTask {
Expand Down Expand Up @@ -554,6 +578,8 @@ final class SFTPServerInboundHandler: ChannelInboundHandler {
symlink(command: command, context: context)
case .readlink(let command):
readlink(command: command, context: context)
case .rename(let command):
rename(command: command, context: context)
case .version, .handle, .status, .data, .attributes, .name:
// Client cannot send these messages
context.channel.triggerUserOutboundEvent(ChannelFailureEvent()).whenComplete { _ in
Expand Down
6 changes: 5 additions & 1 deletion Tests/CitadelTests/Citadel2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ final class Citadel2Tests: XCTestCase {
func addSymlink(linkPath: String, targetPath: String, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
throw DelegateError.unsupported
}


func rename(oldPath: String, newPath: String, flags: UInt32, context: Citadel.SSHContext) async throws -> Citadel.SFTPStatusCode {
throw DelegateError.unsupported
}

func readSymlink(atPath path: String, context: Citadel.SSHContext) async throws -> [Citadel.SFTPPathComponent] {
throw DelegateError.unsupported
}
Expand Down