Skip to content

Instantly share code, notes, and snippets.

@tknerr
Created June 26, 2026 19:44
Show Gist options
  • Select an option

  • Save tknerr/bf6df67348f5b1693f39811843c121c9 to your computer and use it in GitHub Desktop.

Select an option

Save tknerr/bf6df67348f5b1693f39811843c121c9 to your computer and use it in GitHub Desktop.
Monkey-patches for making wconrad/ftpd working on Windows
#
# See https://github.com/wconrad/ftpd (the snippet below has been tested with ftpd gem 2.1.0)
#
# Monkey patches below were AI generated, basic functionality of the patched ftpd server
# has been succesfully tested on Windows 11 using FileZilla as an FTP client
#
require 'ftpd'
require 'tmpdir'
require 'etc'
# Silence the noisy Ruby 3.2 "forwarding to private method" warnings that the
# ftpd gem triggers internally via Forwardable. They are harmless and unrelated
# to anything we can fix in the gem.
module SuppressForwardableWarnings
def warn(message, *args, **kwargs)
return if message.to_s.include?('forwardable.rb') &&
message.to_s.include?('forwarding to private method')
super
end
end
Warning.extend(SuppressForwardableWarnings)
# --------------------------------------------------------------------------
# Windows fix #1: keep the virtual FTP namespace POSIX-based.
#
# Every ftpd command handler normalizes the requested path with
# File.expand_path(argument, name_prefix)
# to resolve "." and ".." against the session's current directory. On Windows
# File.expand_path treats a leading "/" as NOT absolute (it has no drive) and
# helpfully injects the current drive letter:
# File.expand_path('/howto', '/') # => "C:/howto" (Windows)
# # => "/howto" (POSIX)
# That drive letter then leaks into the session's name_prefix, so PWD reports
# "C:\howto", FileZilla treats it as a new root, and subsequent LIST/CWD globs
# resolve against the wrong place (empty or "550 No such file or directory").
#
# We make the virtual namespace behave identically on every OS by routing
# File.expand_path through a pure POSIX implementation whenever the base path
# is a driveless absolute path (i.e. the virtual "/..." namespace). Physical
# disk paths always carry a drive letter (or a relative base) on Windows, so
# they keep Ruby's native behavior untouched.
module PosixVirtualPath
module_function
# True for the virtual FTP namespace: rooted at "/", no Windows drive, not
# a UNC path.
def virtual?(base)
base = base.to_s
base.start_with?('/') && base !~ %r{\A[A-Za-z]:} && base !~ %r{\A//}
end
# Pure POSIX File.expand_path: join + collapse "." and ".." segments
# without ever introducing a drive letter or backslashes.
def expand(path, base)
combined = path.to_s.start_with?('/') ? path.to_s : "#{base}/#{path}"
segments = []
combined.tr('\\', '/').split('/').each do |segment|
case segment
when '', '.' then next
when '..' then segments.pop
else segments << segment
end
end
'/' + segments.join('/')
end
end
class << File
alias_method :__expand_path_native, :expand_path
# Redirect virtual-namespace expansions to the POSIX implementation while
# leaving physical disk paths on Ruby's native (OS-specific) behavior.
def expand_path(path, base = Dir.pwd)
if PosixVirtualPath.virtual?(base)
PosixVirtualPath.expand(path, base)
else
__expand_path_native(path, base)
end
end
# Ruby 3.2 removed File.exists?; restore it for any gem code that still
# uses it.
alias_method :exists?, :exist?
end
# Windows fix #2: file listings.
#
# DiskFileSystem#file_info resolves the owner/group columns via
# Etc.getpwuid / Etc.getgrgid. Those POSIX lookups return nil on Windows,
# causing a NoMethodError during LIST. Fall back to the numeric id.
class WindowsSafeDiskFileSystem < Ftpd::DiskFileSystem
private
def uid_name(uid)
entry = Etc.getpwuid(uid) rescue nil
entry ? entry.name : uid.to_s
end
def gid_name(gid)
entry = Etc.getgrgid(gid) rescue nil
entry ? entry.name : gid.to_s
end
end
class FtpDriver
def initialize(user, password, data_dir)
@user = user
@password = password
@data_dir = data_dir
end
def authenticate(user, password, _account)
user == @user && (password.nil? || password == @password)
end
def file_system(_user)
WindowsSafeDiskFileSystem.new(@data_dir)
end
end
current_dir = File.expand_path(File.dirname(File.dirname(__FILE__)))
driver = FtpDriver.new('user', 'pass', current_dir)
server = Ftpd::FtpServer.new(driver)
server.start
puts "Server listening on port #{server.bound_port}"
gets
server.stop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment