#!/usr/bin/ruby require 'tk' require 'net/ftp' # Close the connection and terminate pgm. def term(conn) if conn begin conn.quit ensure conn.close end end exit end # Display an error dialog. def thud(title, message) Tk.messageBox('icon' => 'error', 'type' => 'ok', 'title' => title, 'message' => message) end # This is the login window. It pops up and asks for the remote host and the # user credentials, and a button to initiate the login when the fields are # ready. class LoginWindow # Generate s label/entry pair for the login window. These will be # appropriately gridded on row row inside par. Text box has width # width and places its contents into the reference $ref. If $ispwd, # treat it as a password entry box. Returns the text variable which # gives access to the entry. def genpair(row, text, width, ispwd=false) tbut = TkLabel.new(@main, 'text' => text) { grid('row' => row, 'column' => 0, 'sticky' => 'nse') } tvar = TkVariable.new('') lab = TkEntry.new(@main) { background 'white' foreground 'black' textvariable tvar width width grid('row' => row, 'column' => 1, 'sticky' => 'nsw') } lab.configure('show' => '*') if ispwd return tvar end # Log into the remote host. If successful, start the directory loader. # Modes are: 1: Anonymous, 2: User, 3: Return, which does anon if the # user infor was not filled in, and user otw. def do_login(mode) host = @host.value acct = @acct.value password = @password.value # Adjust user data by mode. if mode == 1 || (mode == 3 && acct == "" && password == "") acct ='anonymous' if password == "" password = 'anonymous' end end # Make sure we're all filled in. if host == "" || acct == "" || password == "" thud('No Login Info', "You must provide a hostname and login credentials.") return end # Attempt to connect to the remote host and log in begin @conn = Net::FTP.new(host, acct, password) @conn.passive = true rescue thud("Login Failed", $!) @conn = nil return end # Display the listing in the window. @listwin.setconn(@conn) @main.destroy() end def initialize(main, listwin, titfont, titcolor) @main = TkToplevel.new(main) @main.title('FTP Login') # Listing window. @listwin = listwin @conn = nil # This counts through the rows, which makes it easier to modify # the program. row = -1 # Label at the top of window. toplab = TkLabel.new(@main) { text "FTP Server Login" justify 'center' font titfont foreground titcolor grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } # Hostname entry @host = genpair(row += 1, 'Host:', 25) # Login buttons, in a frame for layout. bframe = TkFrame.new(@main) { grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } TkButton.new(bframe, 'command' => proc { self.do_login(1) }) { text 'Anon. Login' pack('side' => 'left', 'expand' => 'yes', 'fill' => 'both') } TkButton.new(bframe, 'command' => proc { self.do_login(2) }) { text 'User Login' pack('side' => 'left', 'expand' => 'yes', 'fill' => 'both') } # Login and password entries. @acct = genpair(row += 1, 'Login:', 15) @password = genpair(row += 1, 'Password:', 15, true) stop = TkButton.new(@main, 'command' => proc { term(@conn) } ) { text 'Exit' grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2, 'sticky' => 'news') } # CR same as pushing login. @main.bind('Return', proc { self.do_login(3) }) end end # This is a window containing the file listing. class FileWindow < TkFrame def initialize(main) super # Set up the title appearance. titfont = 'arial 16 bold' titcolor = '#228800' @conn = nil # Label at top. TkLabel.new(self) { text 'FTP Download Agent' justify 'center' font titfont foreground titcolor pack('side' => 'top', 'fill' => 'x') } # Status label. @statuslab = TkLabel.new(self) { text 'Not Logged In' justify 'center' pack('side' => 'top', 'fill' => 'x') } # Exit button TkButton.new(self) { text 'Exit' command { term(@conn) } pack('side'=> 'bottom', 'fill' => 'x') } # List area with scroll bar. The list area is disabled since we # don't want the user to type into it. @listarea = TkText.new(self) { height 10 width 40 cursor 'sb_left_arrow' state 'disabled' pack('side' => 'left') yscrollbar(TkScrollbar.new).pack('side' => 'right', 'fill' => 'y') } # Bind the system exit button to our exit. main.protocol('WM_DELETE_WINDOW', proc { term(@conn) } ) # Create the login window. LoginWindow.new(main, self, titfont, titcolor) end # Change the color of a tag for entering and leaving. Unfortunately, there # is no active color for tags in a text box. def recolor(tag, color) @listarea.tag_configure(tag, 'foreground' => color) end # Do a CD and load the contents. If there is no directory name, skip # the CD. def load_dir(dir) if dir begin @conn.chdir(dir) rescue thud('No ' + dir, $!) end @statuslab.configure('text' => "[Loading " + dir + "]") else @statuslab.configure('text' => '[Loading Home Dir]') end update # Get the list of files. files = [ ] dirs = [ ] sawdots = false @conn.list() do |line| # Real lines start with the perm bits. And we don't want specials. if line =~ /^[\-d]([r\-][w\-][x\-]){3}/ # Extract the useful parts, toss the bones. The limit keeps us from # dividing file names containing spaces. parts = line.split(/\s+/, 9) if parts.length >= 9 fn = parts.pop() sawdots = true if fn == '..' if parts[0][0..0] == 'd' dirs.push(fn) else files.push(fn) end end end end # Add .. if not present, then sort the list. dirs.push('..') unless sawdots files.sort! dirs.sort! # Clear the old contents from the directory listing box. @listarea.configure('state' => 'normal') @listarea.delete('1.0', 'end') # Fill in the directories. Bind for directory load (us). ct = 0 while fn = dirs.shift tagname = "fn" + ct.to_s @listarea.insert('end', fn+"\n", tagname) @listarea.tag_configure(tagname, 'foreground' => '#4444FF') @listarea.tag_bind(tagname, 'Button-1', proc { |f| self.load_dir(f) }, fn) @listarea.tag_bind(tagname, 'Enter', proc { |t| self.recolor(t, '#0000aa') }, tagname) @listarea.tag_bind(tagname, 'Leave', proc { |t| self.recolor(t, '#4444ff') }, tagname) ct += 1 end # Fill in the files. Bind for download. while fn = files.shift tagname = "fn" + ct.to_s @listarea.insert('end', fn+"\n", tagname) @listarea.tag_configure(tagname, 'foreground' => 'red') @listarea.tag_bind(tagname, 'Button-1', proc { |f| self.dld_file(f) }, fn) @listarea.tag_bind(tagname, 'Enter', proc { |t| self.recolor(t, '#880000') }, tagname) @listarea.tag_bind(tagname, 'Leave', proc { |t| self.recolor(t, 'red') }, tagname) ct += 1 end # Lock it up so the user can't mess with it. @listarea.configure('state' => 'disabled') # Update the status label. begin loc = @conn.pwd() rescue thud('PWD Failed', $!) loc = '???' end @statuslab.configure('text' => loc) end # Download the file. def dld_file(fn) # Announce. @statuslab.configure('text' => "[Retrieving " + fn + "]") update # Get the file. begin @conn.getbinaryfile(fn) rescue thud('DLD Failed', fn + ': ' + $!) @statuslab.configure('text' => '') else @statuslab.configure('text' => 'Got ' + fn) end end # This is a hook that the login window calls after a successful login. # The login window makes the connection and attempts to login. When this # succeeds, it calls setconn() and destroys itself. Setconn records the # connection (which the login box created), then does the initial # directory load. def setconn(conn) @conn = conn load_dir(nil) end end # Create the main window, set the default colors, create the GUI, then # fire the sucker up. BG = '#E6E6FA' root = TkRoot.new('background' => BG) { title "FTP Download" } TkOption.add("*background", BG) TkOption.add("*activebackground", '#FFE6FA') TkOption.add("*foreground", '#0000FF') TkOption.add("*activeforeground", '#0000FF') FileWindow.new(root).pack() Tk.mainloop