#!/usr/local/bin/ruby =begin RubyRED The Ruby Remote Editor Author: Hal Fulton hal9000@hypermetrics.com License: Ruby's Version: 1.0 alpha 16 May 2002 It's not truly an editor; it invokes the local editor you specify. It allows the editing of remote files by (more or less transparently) downloading and uploading files via FTP. It tries very hard to safeguard against overwriting "good" files with "bad" ones, even to the point of experimentally determining the clock difference between the two machines (including any time zone difference). It does not handle binary files. As for text files, since FTP is used, the "newline problem" between Windows and UNIX systems is handled automatically. Right now, it has the clumsiest text interface imaginable. I plan to write a Tk version soon. (This will hopefully be easier because I have encapsulated the user interface methods in a module.) I do plan other GUI versions later, perhaps as a way of comparing their look and feel. A known problem on Windows: If you use Notepad as an editor, and the file is too big (thus prompting you to invoke WordPad), the "system" call will NOT block if you use WordPad. (Presumably Notepad is in effect "forking" the WordPad app.) The fix: Why are you using Notepad anyway? See also the rubyred.html file which is the only documentation. =end require 'net/ftp' require 'singleton' module UI_Tk require "tk" def ui_init @root = TkRoot.new { title "RubyRED" } getOptions unless readConfig start_session @fr1 = TkFrame.new(@root) getOptionsProc = proc { getOptions } quitProc = proc { end_session; exit } getOptProc = proc { getOptions } @btnOptions = TkButton.new(@fr1) do text "Options" command getOptProc pack("side"=>"left") end @btnQuit = TkButton.new(@fr1) do text "Quit" command quitProc pack("side"=>"left") end @fr2 = TkFrame.new(@root) newFilesProc = proc { fnameInit; newFiles } newLocalProc = proc { fnameInit; newLocal } newRemoteProc = proc { fnameInit; newRemote } existingFilesProc = proc { fnameInit; existingFiles } @btnEditNewFiles = TkButton.new(@fr2) do text "Edit: New Files " font "Courier" command newFilesProc pack end @btnEditNewLocal = TkButton.new(@fr2) do text "Edit: New LOCAL File " font "Courier" command newLocalProc pack end @btnEditNewRemote = TkButton.new(@fr2) do text "Edit: New REMOTE File" font "Courier" command newRemoteProc pack end @btnEditExisting = TkButton.new(@fr2) do text "Edit: Existing Files " font "Courier" command existingFilesProc pack end @fr1.pack @fr2.pack end def fnameInit @fnameFrame = TkToplevel.new(@root) locvar = TkVariable.new remvar = TkVariable.new killFrame = proc { @fnameFrame.destroy } TkLabel.new(@fnameFrame) do text "Local file" pack("anchor"=>"w") end TkEntry.new(@fnameFrame) do width 20 textvariable locvar bind "Return", killFrame pack("anchor"=>"w") end TkLabel.new(@fnameFrame) do text "Remote file" pack("anchor"=>"w") end TkEntry.new(@fnameFrame) do width 20 textvariable remvar bind "Return", killFrame pack("anchor"=>"w") end @fnameFrame.wait_destroy @local = locvar.value @remote = remvar.value end def getOptions @optWin = TkToplevel.new(@root) fr1 = TkFrame.new(@optWin) fr2 = TkFrame.new(@optWin) fr3 = TkFrame.new(@optWin) fr4 = TkFrame.new(@optWin) fr5 = TkFrame.new(@optWin) fr6 = TkFrame.new(@optWin) fr7 = TkFrame.new(@optWin) # domain, user, pass, local, remote, editor domain = TkVariable.new user = TkVariable.new pass = TkVariable.new local = TkVariable.new remote = TkVariable.new edpath = TkVariable.new domain.value = @domain user.value = @user pass.value = @passwd local.value = @localDir remote.value = @remoteDir edpath.value = @editor killWin = proc { @optWin.destroy } TkLabel.new(fr1) { text "FTP site"; pack("anchor"=>"w") } TkEntry.new(fr1) do width 20 textvariable domain pack("anchor"=>"w") end TkLabel.new(fr2) { text "User"; pack("anchor"=>"w") } TkEntry.new(fr2) do width 20 textvariable user pack("anchor"=>"w") end TkLabel.new(fr3) { text "Password"; pack("anchor"=>"w") } TkEntry.new(fr3) do width 20 textvariable pass pack("anchor"=>"w") end TkLabel.new(fr4) { text "Local dir"; pack("anchor"=>"w") } TkEntry.new(fr4) do width 20 textvariable local pack("anchor"=>"w") end TkLabel.new(fr5) { text "Remote dir"; pack("anchor"=>"w") } TkEntry.new(fr5) do width 20 textvariable remote pack("anchor"=>"w") end TkLabel.new(fr6) { text "Editor path"; pack("anchor"=>"w") } TkEntry.new(fr6) do width 20 textvariable edpath pack("anchor"=>"w") end TkButton.new(fr7) do text "OK" command killWin bind "Return", killWin pack end fr1.pack fr2.pack fr3.pack fr4.pack fr5.pack fr6.pack fr7.pack @optWin.wait_destroy @domain = domain.value @user = user.value @passwd = pass.value @localDir = local.value @remoteDir = remote.value @editor = edpath.value end def ui_eventloop Tk.mainloop end def ui_notify_local_unchanged end def ui_notify_remote_newer end def ui_notify_uploading end end module UI_text def ui_init getOptions unless readConfig @cmd = nil mainMenu until @cmd =~ /Q/i end def ui_eventloop end def mainMenu puts puts "Main Menu" puts " O Options" puts " E Edit" puts " Q Quit" until @cmd =~ /^[OEQ]$/i print "Command = " @cmd = gets.chomp end case @cmd when /O/i optMenu until @cmd =~ /Q/i @cmd = nil when /E/i edMenu until @cmd =~ /Q/i @cmd = nil end end def optMenu puts puts "Options Menu" puts " 1 Set domain/user/passwd" puts " 2 Set default local dir" puts " 3 Set default remote dir" puts " 4 Set default editor path" puts " S Save options and quit" puts " Q Quit without saving options" until @cmd =~ /^[1234SQ]$/i print "Command = " @cmd = gets.chomp end case @cmd when "1" @domain = getfield("FTP site",@domain) @user = getfield("User",@user) if @user == "anonymous" @user = nil else @passwd = getfield("Password",@passwd) end when "2" @localDir = getfield("Local dir",@localDir) when "3" @remoteDir = getfield("Remote dir",@remoteDir) when "4" @editor = getfield("Editor path",@editor) when /S/i writeConfig @cmd = "Q" when /Q/i # Do nothing... end @cmd = nil unless @cmd =~ /Q/i end def edMenu puts puts "Edit Menu" puts " 1 New files" puts " 2 New LOCAL file" puts " 3 New REMOTE file" puts " 4 Existing files" puts " Q Quit" until @cmd =~ /^[1234Q]$/i print "Command = " @cmd = gets.chomp end if @cmd !~ /Q/i # We ARE editing... @local = getfield("Local file") @remote = getfield("Remote file",@local) end begin start_session case @cmd when "1" newFiles when "2" newLocal when "3" newRemote when "4" existingFiles when /Q/i # Do nothing... end rescue Existence => errmsg puts puts errmsg puts ensure end_session @cmd = nil unless @cmd =~ /Q/i end end def getfield(prompt,default=nil) defstr = if default then "(Enter=#{default})" else "" end print "#{prompt} #{defstr}: " str = gets.chomp if default and str=="" default else str end end def getOptions @domain = getfield("FTP site") @user = getfield("User","anonymous") if @user == "anonymous" @user = nil else @passwd = getfield("Password") end @localDir = getfield("Local dir",@localDir) @remoteDir = getfield("Remote dir",@remoteDir) @editor = getfield("Editor path",@editor) writeConfig end def ui_notify_local_unchanged puts "\n Local file was unchanged!" end def ui_notify_remote_newer puts "\n Remote file is newer... downloading." end def ui_notify_uploading puts "Uploading..." end end ############################## class Existence < Exception end class RubyRED include Singleton include UI_text # include UI_Tk def unix? end def initialize @domain = nil @user = "anonymous" @passwd = "nopass" @localDir = "." @remoteDir = "." @editor = "notepad.exe" @local = nil @locMod = nil # Local file modification time @remote = nil ui_init ui_eventloop end def readConfig begin cfg = File.open("rubyred.cfg") @domain = cfg.gets.chomp @user = cfg.gets.chomp @passwd = cfg.gets.chomp @localDir = cfg.gets.chomp @remoteDir = cfg.gets.chomp @editor = cfg.gets.chomp cfg.close rescue return false end true end def writeConfig File.open("rubyred.cfg","w") do |cfg| cfg.puts @domain cfg.puts @user cfg.puts @passwd cfg.puts @localDir cfg.puts @remoteDir cfg.puts @editor end end def start_ftp args = [@domain] args << @user << @passwd unless @user == nil @ftp = Net::FTP.new(*args) @ftp.chdir(@remoteDir) # check error later end def start_session start_ftp @saveDir = Dir.pwd Dir.chdir(@localDir) end def end_session @ftp.close Dir.chdir(@saveDir) end def upload ui_notify_uploading begin @ftp.puttextfile(@local, @remote) rescue if !tried tried = true start_ftp retry end end end def download tried = false begin @ftp.gettextfile(@remote, @local) rescue if !tried tried = true start_ftp retry end end end def edit @locMod = File.mtime(@local) if File.exist?(@local) system("#@editor #@local") end def unchanged # Is the local file unchanged after editing? didntExist = (@locSize==nil) doesntExist = !File.exist?(@local) sizeSame = (File.size(@local)==@locSize) unless doesntExist timeSame = (File.mtime(@local)==@locMod) unless doesntExist flag = ((didntExist and doesntExist) or (sizeSame and timeSame)) ui_notify_local_unchanged if flag flag end def mdtm2time(str) Time.local(str[0..3].to_i, # year str[4..5].to_i, # month str[6..7].to_i, # day str[8..9].to_i, # hour str[10..11].to_i, # minute str[12..13].to_i, # second 0) # microsec end def remote_time if @ftp.dir(@remote)==[] result = nil else str = @ftp.mdtm(@remote) result = mdtm2time(str) end result end def clockCheck tempfile = "clk-skew" File.open(tempfile,"w") {|f| f.puts "xyz" } # Open-close @ftp.puttextfile(tempfile, tempfile) rmod = @ftp.mdtm(tempfile) lmod = File.mtime(tempfile) rmod = mdtm2time(rmod) skew = lmod - rmod # puts "skew = #{skew} seconds" @ftp.delete(tempfile) # Remote File.delete(tempfile) # Local skew end def check_existence(loc,rem) # Don't forget to save info for "unchanged" # And allow for a "still nonexistent" local file locExists = File.exist?(@local) @remTime = remote_time remExists = (@remTime != nil) @locSize = nil @locMod = nil # locCRC later... if locExists @locSize = File.size(@local) @locMod = File.mtime(@local) end # While we're here, check for "clock skew" @clock_offset = clockCheck # client minus server @remAdjTime = nil if remExists @remAdjTime = @remTime + @clock_offset end err = "" case [loc,rem] when [:noLocal, :noRemote] err << "Local file #@local already exists.\n" if locExists err << "Remote file #@remote already exists.\n" if remExists when [:noLocal, :remote] err << "Local file #@local already exists.\n" if locExists err << "Remote file #@remote does not exist.\n" if !remExists when [:local, :noRemote] err << "Local file #@local does not exist.\n" if !locExists err << "Remote file #@remote already exists.\n" if remExists when [:local, :remote] err << "Local file #@local does not exist.\n" if !locExists err << "Remote file #@remote does not exist.\n" if !remExists end raise Existence.new(err) if err != "" end def newFiles check_existence(:noLocal, :noRemote) edit upload unless unchanged end def newLocal check_existence(:noLocal, :remote) download edit upload unless unchanged end def newRemote check_existence(:local, :noRemote) edit upload # whether it's changed or not end def syncFiles if @remAdjTime > @locMod ui_notify_remote_newer download end end def existingFiles check_existence(:local, :remote) syncFiles edit upload unless unchanged end end RubyRED.instance