require 'ruby_smb/server/share/provider/disk' require 'ruby_smb/server/share/provider/virtual_disk/virtual_file' require 'ruby_smb/server/share/provider/virtual_disk/virtual_pathname' require 'ruby_smb/server/share/provider/virtual_disk/virtual_stat' module RubySMB class Server module Share module Provider # This is a share provider that exposes a virtual file system whose entries do not exist on disk. # @since 3.1.1 class VirtualDisk < Disk HASH_METHODS = %i[ [] each each_key each_value keys length values ] private_constant :HASH_METHODS # @param [String] name The name of this share. def initialize(name) @vfs = {} super(name, add(VirtualPathname.new(self, File::SEPARATOR))) end # Add a dynamic file to the virtual file system. A dynamic file is one whose contents are generated by the # specified block. The contents are generated when the share is opened. # # @param [String] path The path of the file to add, relative to the share root. # @param [File::Stat] stat An explicit stat object describing the file. # @yield The content generation routine. # @yieldreturn [String] The generated file content. def add_dynamic_file(path, stat: nil, &block) raise ArgumentError.new('a block must be specified for dynamic files') unless block_given? path = VirtualPathname.cleanpath(path) path = File::SEPARATOR + path unless path.start_with?(File::SEPARATOR) raise ArgumentError.new('must be a file') if stat && !stat.file? vf = VirtualDynamicFile.new(self, path, stat: stat, &block) add(vf) end # Add a mapped file to the virtual file system. A mapped file is one who is backed by an entry on # disk. The path need not be present, but if it does exist, it must be a file. # # @param [String] path The path of the file to add, relative to the share root. # @param [String, Pathname] mapped_path The path on the local file system to map into the virtual file system. def add_mapped_file(path, mapped_path) path = VirtualPathname.cleanpath(path) path = File::SEPARATOR + path unless path.start_with?(File::SEPARATOR) vf = VirtualMappedFile.new(self, path, mapped_path) add(vf) end # Add a static file to the virtual file system. A static file is one whose contents are known at creation time # and do not change. If *content* is a file-like object that responds to #read, the data will be read using # that method. Likewise, if *content* has a #stat attribute and *stat* was not specified, then the value of # content's #stat attribute will be used. # # @param [String] path The path of the file to add, relative to the share root. # @param [String, #read, #stat] content The static content to add. # @param [File::Stat] stat An explicit stat object describing the file. def add_static_file(path, content, stat: nil) path = VirtualPathname.cleanpath(path) path = File::SEPARATOR + path unless path.start_with?(File::SEPARATOR) raise ArgumentError.new('must be a file') if stat && !stat.file? stat = content.stat if content.respond_to?(:stat) && stat.nil? content = content.read if content.respond_to?(:read) vf = VirtualStaticFile.new(self, path, content, stat: stat) add(vf) end def add(virtual_pathname) raise ArgumentError.new('paths must be absolute') unless virtual_pathname.absolute? path = virtual_pathname.to_s raise ArgumentError.new('paths must be normalized') unless VirtualPathname.cleanpath(path) == path path_parts = path.split(VirtualPathname::SEPARATOR) 2.upto(path_parts.length - 1) do |idx| ancestor = path_parts[0...idx].join(path[VirtualPathname::SEPARATOR]) next if @vfs[ancestor]&.directory? @vfs[ancestor] = VirtualPathname.new(self, ancestor, stat: VirtualStat.new(directory?: true)) end @vfs[path] = virtual_pathname end def new_processor(server_client, session) scoped_virtual_disk = self.class.new(@name) @vfs.each_value do |path| path = path.dup path.virtual_disk = scoped_virtual_disk if path.is_a?(VirtualPathname) path = path.generate(server_client, session) if path.is_a?(VirtualDynamicFile) scoped_virtual_disk.add(path) end self.class::Processor.new(scoped_virtual_disk, server_client, session) end private def method_missing(symbol, *args) if HASH_METHODS.include?(symbol) return @vfs.send(symbol, *args) end raise NoMethodError, "undefined method `#{symbol}' for #{self.class}" end def respond_to_missing?(symbol, include_private = false) HASH_METHODS.include?(symbol) end end end end end end