"""
FtpCube
Copyright (C) Michael Gilfix
This file is part of FtpCube.
You should have received a file COPYING containing license terms
along with this program; if not, write to Michael Gilfix
(mgilfix@eecs.tufts.edu) for a copy.
This version of FtpCube is open source; you can redistribute it and/or
modify it under the terms listed in the file COPYING.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
"""
import browser
import cache
import chmodwin
import dispatcher
import events
import icons.folder
import icons.link
import icons.file
import logger
import messages
import protocol
import utils
from logger import Logger
import wx
import os
import re
import time
import sys
class AbstractFileWindow(wx.Panel):
"""Base class for file browsing windows.
A file browsing window provides a directory navigation window, where the window displays
the contents of a directory. A file window allows for sorting of the displayed files,
with directories first and hiding of hidden entries (those that start with a '.'). The
file display can be sorted by various columns, including name, size, and date.
An abstact file window handles two types of events by default: double clicks that
perform entry into a directory and right clicks which bring up a popup menu. The
popup menu should be populated with actions that can be performed on the entry for the
given type of file window."""
# Sorting constants
UNSORTED = 0
SORT_BY_NAME = 1
SORT_BY_SIZE = 2
SORT_BY_DATE = 3
idLIST = wx.NewId()
HIDDEN_RE = re.compile("^\..*")
def __init__(self, parent, headers):
"""Creates a displays the file window.
'headers' is a list of headers for columns in the file window."""
wx.Panel.__init__(self, parent, -1)
self.list = wx.ListCtrl(self, self.idLIST, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
for col, name in zip(range(len(headers)), headers):
self.list.InsertColumn(col, name)
self.image_list = wx.ImageList(15, 17)
self.folder_index = self.image_list.Add(icons.folder.getBitmap())
self.link_index = self.image_list.Add(icons.link.getBitmap())
self.file_index = self.image_list.Add(icons.file.getBitmap())
self.list.SetImageList(self.image_list, wx.IMAGE_LIST_SMALL)
status_bar = self.makeStatusBar()
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.list, 1, wx.EXPAND)
sizer.Add(status_bar, 0, wx.EXPAND)
self.SetAutoLayout(True)
self.SetSizer(sizer)
# Set instance variables
self.sorted = self.SORT_BY_NAME
self.hidden_files = True
self.directories_first = True
self.dir = None
self.list.Bind(wx.EVT_LEFT_DCLICK, self.onDoubleClick)
self.list.Bind(wx.EVT_RIGHT_DOWN, self.onRightClick)
def makeStatusBar(self):
"""Creates the status bar for displaying the current directory and any transfer
information."""
self.status_label = wx.StaticText(self, -1, "")
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add((5, 1))
hsizer.Add(self.status_label, 1, wx.EXPAND)
vsizer = wx.BoxSizer(wx.VERTICAL)
vsizer.Add((0, 3))
vsizer.Add(hsizer, 0, wx.EXPAND)
vsizer.Add((0, 3))
return vsizer
def getStatusBar(self):
"""Returns a wx StaticText object that is the status bar."""
return self.status_label
def updateStatusBar(self, dir):
"""Updates the status bar with the specified directory text."""
status_bar = self.getStatusBar()
status_bar.SetLabel(dir)
def selectBitmapIndex(self, flags):
"""Selects the appropriate bitmap to use given a series of file flags.
File flags are in the string form used in unix file listings."""
if not flags:
return self.folder_index
if flags[0] == 'l':
return self.link_index
elif flags[0] == 'd':
return self.folder_index
else:
return self.file_index
def setDir(self, dir):
"""Sets the current directory for the file window."""
self.dir = dir
def getDir(self):
"""Returns the current directory for the file window."""
return self.dir
def onDoubleClick(self, event):
"""Handles a double click event on the file window."""
raise NotImplementedError
def onRightClick(self, event):
"""Handles a right click event on the file window."""
raise NotImplementedError
def toggleHide(self, event=None):
"""Toggles the hidden files flag."""
self.hidden_files = bool((self.hidden_files + 1) % 2)
self.updateListing('.')
def toggleDirectoriesFirst(self, event=None):
"""Toggles the directories first flag."""
self.directories_first = bool((self.directories_first + 1) % 2)
self.updateListing('.')
def updateListing(self, dir):
"""Updates the file window to reflect the listing for the specified directory."""
raise NotImplementedError
def unsorted(self, event=None):
"""Sets the sort mode to unsorted and updates the file listing."""
self.sorted = self.UNSORTED
self.directories_first = False
self.updateListing('.')
def sortByName(self, event=None):
"""Sets the sort mode to sorting by name and updates the file listing."""
self.sorted = self.SORT_BY_NAME
self.directories_first = True
self.updateListing('.')
def sortBySize(self, event=None):
"""Sets the sort mode to sorting by size and updates the file listing."""
self.sorted = self.SORT_BY_SIZE
self.directories_first = True
self.updateListing('.')
def sortByDate(self, event=None):
"""Sets the sort mode to sorting by date and updates the file listing."""
self.sorted = self.SORT_BY_DATE
self.directories_first = True
self.updateListing('.')
def clearList(self):
"""Clears the current list of files."""
if self.list:
self.list.Freeze()
self.list.DeleteAllItems()
self.list.Thaw()
def sortList(self, sequence):
"""Performs the sorting of a sequence of entries for the file list.
The actual sorting of the list is implementation dependent."""
raise NotImplementedError
def performSort(self, sequence):
"""Performs a low-level sorting of a list names.
A sorted list of file entries is returned. The sort mode is taken into account while
sorting. This method does distinguish between sorting directories first."""
if self.sorted == self.SORT_BY_NAME:
sequence.sort(lambda x, y: cmp(x[0], y[0]))
elif self.sorted == self.SORT_BY_SIZE:
sequence.sort(lambda x, y: cmp(long(x[4]), long(y[4])))
elif self.sorted == self.SORT_BY_DATE:
# Convert the times beforehand for speed
try:
converted = [ (i, time.strptime(i[2], "%b %d %H:%M")) for i in sequence ]
except Exception:
return sequence
converted.sort(lambda x, y: comp(x[1], y[1]))
sequence = [ i[0] for i in converted ]
return sequence
def getFileSize(self, file):
"""Returns the size of the specified file name."""
index = self.list.FindItem(-1, file)
return utils.getColumnText(self.list, index, 2)
def getMainThread(self):
"""Returns the main thread from the application thread manager."""
thread_mgr = utils.getAppThreadManager()
return thread_mgr.getMainThread()
class LocalWindow(AbstractFileWindow):
"""The local file browsing window.
This window allows for browsing the local filesystem. All operations in popup menus are
performed on local files. Browsing is subject to the permissions of directories and
files. Operations on local files that result in tranfers to remote systems make use of
the main connection thread for execution."""
# Popup IDs
idPOPUP_UPLOAD = wx.NewId()
idPOPUP_RENAME = wx.NewId()
idPOPUP_DELETE = wx.NewId()
idPOPUP_CREATE = wx.NewId()
idPOPUP_CHMOD = wx.NewId()
idPOPUP_CHANGE = wx.NewId()
idPOPUP_REFRESH = wx.NewId()
idPOPUP_HIDDEN = wx.NewId()
idPOPUP_UNSORT = wx.NewId()
idPOPUP_BYNAME = wx.NewId()
idPOPUP_BYSIZE = wx.NewId()
idPOPUP_BYDATE = wx.NewId()
idPOPUP_ORDER = wx.NewId()
TIME_RE = re.compile(('[A-Za-z]+\s+([A-Za-z]+\s+\d+\s+\d+:\d+):(\d+)\s+\d+'))
def __init__(self, parent):
"""Creates the local file browsing window and starts the window in the default
directory for the local OS."""
if __debug__:
print "Making local file browsing window."
self.headers = [ _("Filename"), _("Size"), _("Date"), _("Flags") ]
AbstractFileWindow.__init__(self, parent, self.headers)
# Set the date alignment
utils.setListColumnAlignment(self.list, 2, wx.LIST_FORMAT_RIGHT)
# Set the heading sizes accordingly
for i in range(len(self.headers)):
self.list.SetColumnWidth(i, 100)
# Explicitly make the filenames column bigger
self.list.SetColumnWidth(0, 200)
# Open up the default directory for display
cwd = None
if sys.platform == 'win32' and os.environ.has_key('HOMEPATH') and \
os.environ.has_key('HOMEDRIVE'):
cwd = os.environ['HOMEDRIVE'] + os.environ['HOMEPATH']
elif os.name == 'posix' and os.environ.has_key('HOME'):
cwd = os.environ['HOME']
if cwd is None:
cwd = os.getcwd()
self.setDir(cwd)
self.last_listing = None
self.updateListing(self.getDir())
self.Bind(events.EVT_LOCAL_WINDOW, self.onLocalWindowEvent)
evt_registry = events.getEventRegistry()
evt_registry.registerEventListener(events.EVT_LOCAL_WINDOW_TYPE, self)
def onDoubleClick(self, event):
"""Handles a double-click event.
If a directory is double clicked, then that directory is entered. If a file is
double-clicked, then a no-op occurs."""
item = self.list.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
if item != -1:
selected = utils.getColumnText(self.list, item, 0)
self.updateListing(selected)
def onRightClick(self, event):
"""Creates a popup menu at the location of the right click."""
menu = self.makePopupMenu()
self.PopupMenu(menu, event.GetPosition())
def onLocalWindowEvent(self, event):
"""Handles custom local window events.
Local window events are an update to the window and a refresh of the window.
The update uses the event message as the updated directory and causes a change
in the view. The refresh reloads the current directory."""
if event.kind == events.LocalWindowEvent.EVT_LIST_UPDATE:
self.updateListing(event.msg)
elif event.kind == events.LocalWindowEvent.EVT_LIST_REFRESH:
self.onRefresh(event)
def makePopupMenu(self):
"""Creates a popup menu containing the list of supported actions for local file
objects."""
menu = wx.Menu()
menu.Append(self.idPOPUP_UPLOAD, _("Upload Files"))
menu.Append(self.idPOPUP_RENAME, _("Rename Files"))
menu.Append(self.idPOPUP_DELETE, _("Delete Selected Files"))
menu.AppendSeparator()
menu.Append(self.idPOPUP_CREATE, _("Create Directory"))
menu.Append(self.idPOPUP_CHMOD, _("Change Permissions"))
menu.Append(self.idPOPUP_CHANGE, _("Change Directory"))
menu.Append(self.idPOPUP_REFRESH, _("Refresh Listing"))
menu.AppendSeparator()
menu.AppendCheckItem(self.idPOPUP_HIDDEN, _("Hide Hidden Files"))
menu.AppendSeparator()
# Create the sort order sub menu and add it
sort_menu = wx.Menu()
sort_menu.Append(self.idPOPUP_UNSORT, _("Unsorted"))
sort_menu.Append(self.idPOPUP_BYNAME, _("Sort By Name"))
sort_menu.Append(self.idPOPUP_BYSIZE, _("Sort By Size"))
sort_menu.Append(self.idPOPUP_BYDATE, _("Sort By Date"))
sort_menu.AppendSeparator()
sort_menu.AppendCheckItem(self.idPOPUP_ORDER, _("Order Directories-Links-Files"))
menu.AppendMenu(wx.NewId(), _("Sort Order"), sort_menu)
if self.hidden_files:
menu_item = menu.FindItemById(self.idPOPUP_HIDDEN)
menu_item.Check(True)
if self.directories_first:
menu_item = menu.FindItemById(self.idPOPUP_ORDER)
menu_item.Check(True)
self.Bind(wx.EVT_MENU, self.onUpload, id=self.idPOPUP_UPLOAD)
self.Bind(wx.EVT_MENU, self.onRename, id=self.idPOPUP_RENAME)
self.Bind(wx.EVT_MENU, self.onDelete, id=self.idPOPUP_DELETE)
self.Bind(wx.EVT_MENU, self.onCreate, id=self.idPOPUP_CREATE)
self.Bind(wx.EVT_MENU, self.onChmod, id=self.idPOPUP_CHMOD)
self.Bind(wx.EVT_MENU, self.onChange, id=self.idPOPUP_CHANGE)
self.Bind(wx.EVT_MENU, self.onRefresh, id=self.idPOPUP_REFRESH)
self.Bind(wx.EVT_MENU, self.toggleHide, id=self.idPOPUP_HIDDEN)
self.Bind(wx.EVT_MENU, self.unsorted, id=self.idPOPUP_UNSORT)
self.Bind(wx.EVT_MENU, self.sortByName, id=self.idPOPUP_BYNAME)
self.Bind(wx.EVT_MENU, self.sortBySize, id=self.idPOPUP_BYSIZE)
self.Bind(wx.EVT_MENU, self.sortByDate, id=self.idPOPUP_BYDATE)
self.Bind(wx.EVT_MENU, self.toggleDirectoriesFirst, id=self.idPOPUP_ORDER)
return menu
def onUpload(self, event):
"""Submits the selected item to the transfer queue for uploading to the remote
site.
Submission is made by posting a queue event to the thread manager. This method
also tries to record the remote size of the file from the remote file window to
determine the resuming size."""
selected = utils.getSelected(self.list)
main_thread = utils.getAppThreadManager().getMainThread()
if not main_thread:
return
opts = main_thread.getOptions()
if opts is None:
return
remote_win = utils.getRemoteWindow()
for item in selected:
file = utils.getColumnText(self.list, item, 0)
if file == '..':
continue
path = os.path.join(self.getDir(), file)
flags = self.last_listing[self.list.GetItemData(item)][3]
# Check if the file exists remotely. If so, replace the file's size with the
# remote size. Later, we'll check if the size is smaller and perform a resume
# if it is
remote_size = remote_win.getTrueFileSize(file)
if remote_size is None:
try:
remote_size = os.path.getsize(path)
except OSError:
remote_size = 0
event = events.EnqueueEvent(
file = file,
host = opts['host'],
port = opts['port'],
username = opts['username'],
password = opts['password'],
size = remote_size,
flags = flags[0], # Just provide the type flag
remote_path = remote_win.getDir(),
local_path = self.getDir(),
direction = protocol.ProtocolInterface.UPLOAD,
attempt = 1,
max_attempts = opts['retries'],
method = utils.getTransferMethod(),
transport = opts['transport'],
)
evt_registry = events.getEventRegistry()
evt_registry.postEvent(event)
def onRename(self, event):
"""Performs a rename the selected entries."""
selected = utils.getSelected(self.list)
for item in selected:
file = utils.getColumnText(self.list, item, 0)
orig_path = os.path.join(self.getDir(), file)
new = messages.displayInputDialog(self, _("Ftpcube - Rename"),
_("Rename %(f)s to:") %{ 'f' : file })
if new:
new_path = os.path.join(self.getDir(), new)
try:
os.rename(orig_path, new_path)
except OSError, strerror:
messages.displayErrorDialog(self,
_("Error renaming file %(orig)s to %(new)s: %(err)s")
%{ 'orig' : orig_path, 'new' : new_path, 'err' : strerror })
return
self.updateListing(self.getDir())
def onDelete(self, event):
"""Deletes the selected entries from the filesystem."""
selected = utils.getSelected(self.list)
for item in selected:
file = utils.getColumnText(self.list, item, 0)
type = self.last_listing[self.list.GetItemData(item)][3]
path = os.path.join(self.getDir(), file)
if type[0] in '-lf':
try:
os.remove(path)
except OSError, strerror:
messages.displayErrorDialog(self, _("Error removing file: %(err)s")
%{ 'err' : strerror })
else:
try:
os.rmdir(path)
except OSError, strerror:
messages.displayErrorDialog(self, _("Error removing directory: %(err)s")
%{ 'err' : strerror })
self.list.SetItemState(item, 0, wx.LIST_STATE_SELECTED)
self.updateListing(self.getDir())
def onCreate(self, event):
"""Creates a new directory in the current directory.
An input dialog box is displayed to get the new directory name."""
dir = messages.displayInputDialog(self, _("Ftpcube - Create Directory"),
_("Directory Name:"))
if dir:
path = os.path.join(self.getDir(), dir)
try:
os.mkdir(path)
except OSError, strerror:
messages.displayErrorDialog(self, _("Error creating directory: %(err)s")
%{ 'err' : strerror })
return
self.updateListing(self.getDir())
def onChmod(self, event):
"""Displays the change permissions window to change the selected files permissions."""
selected = utils.getSelected(self.list)
for item in selected:
file = utils.getColumnText(self.list, item, 0)
path = os.path.join(self.getDir(), file)
try:
info = os.stat(path)
perm = info[0] # st_mode
except OSError, strerror:
messages.displayErrorDialog(self,
_("Error obtaining permissions for %(f)s: %(err)s")
%{ 'f' : file, 'err' : strerror })
continue
chmod_win = chmodwin.ChmodWindow(self, file, perm)
ret = chmod_win.ShowModal()
if ret == wx.ID_OK:
new_perm = chmod_win.getPermissions()
try:
os.chmod(path, new_perm)
except OSError, strerror:
messages.displayErrorDialog(self,
_("Error setting permissions for %(f)s: %(err)s")
%{ 'f' : file, 'err' : strerror })
if selected:
self.updateListing(self.getDir())
def onChange(self, event):
"""Processes a change directory event.
This displays the local browser window, which allows the user to navigate and
select the desired directory. The results of this window is used to change
the current listing."""
local_browser = browser.LocalBrowser(self, self.getDir())
ret = local_browser.ShowModal()
if ret == wx.ID_OK:
dir = local_browser.getDirectory()
if dir:
self.updateListing(dir)
def onRefresh(self, event):
"""Refreshes the current listing."""
self.updateListing('.')
def updateListing(self, dir):
"""Updates the displayed listing for the specified directory.
This method provides special handling for the '.' and '..' directory entries."""
dir = os.path.normpath(dir)
# Construct our new path
if dir == '..':
self.setDir(os.path.dirname(self.getDir()))
elif dir != '.':
newpath = os.path.join(self.getDir(), dir)
if os.path.isdir(newpath):
self.setDir(newpath)
else:
messages.displayErrorDialog(self, _("Invalid directory: %(path)s")
%{ 'path' : newpath })
# Get the new listing
self.last_listing = self.readDir(self.getDir())
self.list.Freeze()
self.clearList()
if self.sorted:
parent_entry = self.last_listing.pop(0)
self.last_listing = self.sortList(self.last_listing)
# Make sure the parent entry is first
self.last_listing.insert(0, parent_entry)
for i in range(len(self.last_listing)):
item = self.last_listing[i]
img_index = self.selectBitmapIndex(item[3])
self.list.InsertImageStringItem(i, item[0], img_index)
self.list.SetStringItem(i, 1, item[1])
self.list.SetStringItem(i, 2, item[2])
self.list.SetStringItem(i, 3, item[3])
self.list.SetItemData(i, i)
self.list.Thaw()
self.updateStatusBar(self.getDir())
def readDir(self, dir):
"""Reads the contents of the specified directory and returns a list of tuples that
corresponds to the appropriate columns for the local file browsing window."""
try:
files = os.listdir(dir)
except OSError, strerror:
messages.displayErrorDialog(self, _("Error reading directory: %(err)s")
%{ 'err' : strerror })
return
if self.hidden_files:
files = [ i for i in files if not self.HIDDEN_RE.match(i) ]
# Add the special file '..'
files.insert(0, '..')
listitems = [ ]
for f in files:
# Attempt to retrieve the file info and modification times
try:
# lstat is preferrable for systems that support it
if os.name == 'posix':
(st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size,
st_atime, st_mtime, st_ctime) = os.lstat(os.path.join(dir, f))
else:
(st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size,
st_atime, st_mtime, st_ctime) = os.stat(os.path.join(dir, f))
except OSError, strerror:
# Set the size and mtime to 0 then and carry on if we can't stat the
# file for some reason but know it's there
st_size = 0
st_mtime = 0
# Beautify the size and time for human consumption
size = utils.beautifySize(st_size)
match = self.TIME_RE.match(time.ctime(st_mtime))
mtime = match.group(1)
# Set the appropriate file type and determine the mode
mode = self.convertPermToStr(st_mode)
if os.path.islink(os.path.join(self.getDir(), f)):
mode = 'l' + mode
elif os.path.isdir(os.path.join(self.getDir(), f)):
mode = 'd' + mode
else:
mode = '-' + mode
listitems.append((f, size, mtime, mode, str(st_size)))
return listitems
def convertPermToStr(self, perm):
"""Converts an octal permission into a string representation.
The string format follows the commonly used format for representing permissions
in unix."""
flags = [ '-' ] * 9
if perm & (4 << 6):
flags[0] = 'r'
if perm & (2 << 6):
flags[1] = 'w'
if perm & (1 << 6):
flags[2] = 'x'
if perm & (4 << 3):
flags[3] = 'r'
if perm & (2 << 3):
flags[4] = 'w'
if perm & (1 << 3):
flags[5] = 'x'
if perm & 4:
flags[6] = 'r'
if perm & 2:
flags[7] = 'w'
if perm & 1:
flags[8] = 'x'
return ''.join(flags)
def sortList(self, list):
"""Sorts a list of file entries.
This sort method separates directories first if that flag is set. Otherwise, the
straight-forward low-level sequent sorting method 'performSort' is used."""
if self.directories_first:
dirs = [ i for i in list if i[3][0] == 'd' ]
links = [ i for i in list if i[3][0] == 'l' ]
files = [ i for i in list if i[3][0] in '-f' ]
list = [ ]
list.extend(self.performSort(dirs))
list.extend(self.performSort(links))
list.extend(self.performSort(files))
else:
list = self.performSort(list)
return list
class RemoteWindow(AbstractFileWindow):
"""Remote site browsing window.
This window allows for browsing directory structures and files on a remote site.
The directory cache is used to allow rapid browsing of already known directory
structures. This window supports numerous operations on remote files through a
right-click popup menu. Remote transfer events are submitted to the thread manager
queue for execution of the transfer."""
# Popup Menu IDs
idPOPUP_DOWNLOAD = wx.NewId()
idPOPUP_RENAME = wx.NewId()
idPOPUP_DELETE = wx.NewId()
idPOPUP_CREATE = wx.NewId()
idPOPUP_CHANGE = wx.NewId()
idPOPUP_CHMOD = wx.NewId()
idPOPUP_REFRESH = wx.NewId()
idPOPUP_UNSORT = wx.NewId()
idPOPUP_BYNAME = wx.NewId()
idPOPUP_BYSIZE = wx.NewId()
idPOPUP_BYDATE = wx.NewId()
idPOPUP_ORDER = wx.NewId()
idPOPUP_CLRCACHE = wx.NewId()
# Regular expressions
LIST_RE = re.compile('([a-z-]+) \s+ (\d+) \s+ (\S+) \s+ (\S+) \s+ (\d+) \s+ ' +
'([a-z\d]+\s+[a-z\d]+\s+[\d:]+) \s+ (.*)', re.IGNORECASE + re.VERBOSE)
MSWIN_LIST_RE = re.compile('([a-z-]+) \s+ (\d+) \s+ ([a-z]+) \s+ (\d+) \s+ ' +
'([a-z]+\s+\d+\s+[\d:]+) \s+ (.*)', re.IGNORECASE + re.VERBOSE)
MSDOS_LIST_RE = re.compile('([\d-]+\s+[a-z\d:]+) \s+ (?: \<(\w+)\> | (\d+) ) ' +
'\s+ (.*)', re.IGNORECASE + re.VERBOSE)
LINK_RE = re.compile('(.*) -> (.*)')
BROKEN_LINK_RE = re.compile('.*-> (.*)')
TIME_RE = re.compile('[A-Za-z]+\s+([A-Za-z]+\s+\d+\s+\d+:\d+):(\d+)\s+\d+')
# Remote separator
REMOTE_SEP = '/'
def __init__(self, parent):
"""Creates and displays the remote file browsing window."""
if __debug__:
print "Making remote file browsing window."
self.headers = [ _("Filename"), _("Size"), _("Date"), _("Flags") ]
AbstractFileWindow.__init__(self, parent, self.headers)
# Set up the headers
utils.setListColumnAlignment(self.list, 2, wx.LIST_FORMAT_RIGHT)
for i in range(len(self.headers)):
self.list.SetColumnWidth(i, 100)
# Explicitly make the filenames column bigger
self.list.SetColumnWidth(0, 200)
self.cache = cache.DirectoryCacheTree()
self.Bind(events.EVT_REMOTE_WINDOW, self.onRemoteWindowEvent)
evt_registry = events.getEventRegistry()
evt_registry.registerEventListener(events.EVT_REMOTE_WINDOW_TYPE, self)
def onDoubleClick(self, event):
"""Processes a double click.
If a directory is double clicked, then the directory is entered. If a file is
double-clicked, then this handler serves as a no-op."""
item = self.list.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
if item != -1:
selected = utils.getColumnText(self.list, item, 0)
thread = self.getMainThread()
if selected == '..':
old_dir = self.getDir()
mydir = self.dirname(old_dir)
clogger = logger.ConsoleLogger()
handler = CwdErrorHandler(old_dir, self, clogger)
thread.cwd('..', err_handler=handler)
self.updateStatusBar(mydir)
self.setDir(mydir)
self.getListing()
else:
type = utils.getColumnText(self.list, item, 3)
if type[0] in 'ld':
old_dir = self.getDir()
mydir = self.join([ old_dir, selected ])
self.changeDirectory(mydir, old_dir)
def dirname(self, dir):
"""Gets the base directory from a directory name.
The remote separator is used rather than the OS separator."""
index = dir.rfind(self.REMOTE_SEP)
if index == 0:
return self.REMOTE_SEP
else:
return dir[:index]
def normpath(self, path):
"""Normalizes the specified path.
This eliminates any sub '.' or '..' entries from the path."""
dirs = path.split(self.REMOTE_SEP)
pathlist = [ ]
i = 0
while i < len(dirs):
if dirs[i] == '.':
pass
elif dirs[i] == '..':
pathlist = pathlist[0:i-2]
elif dirs[i]:
pathlist.append(dirs[i])
i = i + 1
return self.REMOTE_SEP + self.REMOTE_SEP.join(pathlist)
def join(self, args):
"""Joins together a list of path elements by the remote separator and returns a
normalized path."""
return self.normpath(self.REMOTE_SEP.join(args))
def changeDirectory(self, dir, old_dir):
"""Changes the remote directory to the newly specified directory.
If an error occurs, then the 'old_dir' is used to change back to the previous
remote directory."""
main_thread = self.getMainThread()
if main_thread is not None:
console_logger = logger.ConsoleLogger()
handler = CwdErrorHandler(old_dir, self, console_logger)
self.updateStatusBar(dir)
self.setDir(dir)
main_thread.cwd(dir, err_handler=handler)
self.getListing()
def onRightClick(self, event):
"""Processes a right click by displaying a popup menu at the click point."""
menu = self.makePopupMenu()
self.PopupMenu(menu, event.GetPosition())
def makePopupMenu(self):
"""Creates the popup menu containing the list of remote operations."""
menu = wx.Menu()
menu.Append(self.idPOPUP_DOWNLOAD, _("Download Selected"))
menu.Append(self.idPOPUP_RENAME, _("Rename Selected"))
menu.Append(self.idPOPUP_CHMOD, _("Change Permissions"))
menu.Append(self.idPOPUP_DELETE, _("Delete Selected"))
menu.AppendSeparator()
menu.Append(self.idPOPUP_CREATE, _("Create Directory"))
menu.Append(self.idPOPUP_CHANGE, _("Change Directory"))
menu.Append(self.idPOPUP_REFRESH, _("Refresh Cache"))
menu.AppendSeparator()
sort_menu = wx.Menu()
sort_menu.Append(self.idPOPUP_UNSORT, _("Unsorted"))
sort_menu.Append(self.idPOPUP_BYNAME, _("Sort by Name"))
sort_menu.Append(self.idPOPUP_BYSIZE, _("Sort by Size"))
sort_menu.Append(self.idPOPUP_BYDATE, _("Sort by Date"))
sort_menu.AppendSeparator()
sort_menu.AppendCheckItem(self.idPOPUP_ORDER, _("Order Directories-Links-Files"))
menu.AppendMenu(wx.NewId(), _("Sort Order"), sort_menu)
menu.AppendSeparator()
menu.Append(self.idPOPUP_CLRCACHE, _("Clear Directory Cache"))
if self.directories_first:
menu_item = menu.FindItemById(self.idPOPUP_ORDER)
menu_item.Check(True)
self.Bind(wx.EVT_MENU, self.onDownload, id=self.idPOPUP_DOWNLOAD)
self.Bind(wx.EVT_MENU, self.onRename, id=self.idPOPUP_RENAME)
self.Bind(wx.EVT_MENU, self.onDelete, id=self.idPOPUP_DELETE)
self.Bind(wx.EVT_MENU, self.onCreate, id=self.idPOPUP_CREATE)
self.Bind(wx.EVT_MENU, self.onChange, id=self.idPOPUP_CHANGE)
self.Bind(wx.EVT_MENU, self.onChmod, id=self.idPOPUP_CHMOD)
self.Bind(wx.EVT_MENU, self.onRefresh, id=self.idPOPUP_REFRESH)
self.Bind(wx.EVT_MENU, self.unsorted, id=self.idPOPUP_UNSORT)
self.Bind(wx.EVT_MENU, self.sortByName, id=self.idPOPUP_BYNAME)
self.Bind(wx.EVT_MENU, self.sortBySize, id=self.idPOPUP_BYSIZE)
self.Bind(wx.EVT_MENU, self.sortByDate, id=self.idPOPUP_BYDATE)
self.Bind(wx.EVT_MENU, self.toggleDirectoriesFirst, id=self.idPOPUP_ORDER)
self.Bind(wx.EVT_MENU, self.clearCache, id=self.idPOPUP_CLRCACHE)
return menu
def updateStatusBar(self, dir):
"""Updates the status bar indicating the current directory or information about
processing the current directory."""
AbstractFileWindow.updateStatusBar(self, dir)
event = events.DirectoryChangeEvent(dir)
evt_registry = events.getEventRegistry()
evt_registry.postEvent(event)
def onRemoteWindowEvent(self, event):
"""Processes a custom remote window event.
The following events are supported: An update listing event for display of data
within the window, a refresh event for the current directory contents, and a
change of the directory status bar event."""
if event.kind == events.RemoteWindowEvent.EVT_LIST_UPDATE:
listing = event.msg
self.displayFreshListing(listing)
elif event.kind == events.RemoteWindowEvent.EVT_LIST_REFRESH:
if event.msg == self.getDir():
self.onRefresh(event)
elif event.kind == events.RemoteWindowEvent.EVT_CWD:
self.updateStatusBar(event.msg)
self.setDir(event.msg)
def onDownload(self, event):
"""Processes a download event for a selected list of files and directories.
Each entry is submitted to the transfer queue for execution of the transfer
by a connection thread."""
selected = utils.getSelected(self.list)
main_thread = self.getMainThread()
if not main_thread:
return
opts = main_thread.getOptions()
for item in selected:
file = utils.getColumnText(self.list, item, 0)
if file == '..':
continue
flags = utils.getColumnText(self.list, item, 3)
listdata = self.cache.findNode(self.getDir())
if listdata is None or listdata.data is None:
continue
# Extract true size
truesize = 0L
for x in listdata.data:
if x[0] == file:
truesize = long(x[4])
break
event = events.EnqueueEvent(
file = file,
host = opts['host'],
port = opts['port'],
username = opts['username'],
password = opts['password'],
size = truesize,
flags = flags[0],
remote_path = self.getDir(),
local_path = utils.getLocalWindow().getDir(),
direction = protocol.ProtocolInterface.DOWNLOAD,
attempt = 1,
max_attempts = opts['retries'],
method = utils.getTransferMethod(),
transport = opts['transport'],
)
evt_registry = events.getEventRegistry()
evt_registry.postEvent(event)
def onRename(self, event):
"""Processes a rename event for all selected file."""
selected = utils.getSelected(self.list)
thread = self.getMainThread()
for item in selected:
file = utils.getColumnText(self.list, item, 0)
if file == '..':
continue
new_name = messages.displayInputDialog(self, _("Ftpcube - Rename"),
_("Rename %(f)s to:") %{ 'f' : file })
thread.rename(file, new_name)
thread.list()
def onDelete(self, event):
"""Processes a delete event for all selected items."""
selected = utils.getSelected(self.list)
thread = self.getMainThread()
for item in selected:
file = utils.getColumnText(self.list, item, 0)
if file == '..':
continue
type = utils.getColumnText(self.list, item, 3)
if type[0] in '-lf':
thread.delete(file)
else:
thread.rmdir(file)
thread.list()
def onCreate(self, event):
"""Creates a new remote directory."""
thread = self.getMainThread()
dir = messages.displayInputDialog(self, _("Ftpcube - Create Remote Directory"),
_("Directory name:"))
if dir and not dir =='..':
thread.mkdir(dir)
thread.list()
def onChange(self, event):
"""Displays the remote browser window to allow the user to navigate the remote
directory structure and select a directory to change to.
The remote browser makes uses of the cached directory structures and only reflects
the directories that have been previous traversed to. Otherwise, the input box
can be used to input a previously unvisited directory."""
remote_browser = browser.RemoteBrowser(self, self.cache, self.getDir())
ret = remote_browser.ShowModal()
if ret == wx.ID_OK:
dir = remote_browser.getDirectory()
if dir:
self.changeDirectory(dir, self.getDir())
def onChmod(self, event):
"""Brings up the change permissions window to specify permissions to change for
the remote file."""
selected = utils.getSelected(self.list)
thread = self.getMainThread()
for item in selected:
file = utils.getColumnText(self.list, item, 0)
if file == '..':
continue
current_perm = self.getFilePermission(item)
chmod_win = chmodwin.ChmodWindow(self, file, current_perm)
ret = chmod_win.ShowModal()
if ret == wx.ID_OK:
perm = chmod_win.getPermissions()
perm_str = oct(perm)
thread.chmod(file, perm_str)
def getFilePermission(self, selected):
"""Extracts the file permission from a unix text representation into an octal
format."""
flags = utils.getColumnText(self.list, selected, 3)
if not flags:
return None
# Grab the permissions part
flags = flags[1:]
mode = 0
if flags[0] == 'r':
mode |= (4 << 6)
if flags[1] == 'w':
mode |= (2 << 6)
if flags[2] == 'x':
mode |= (1 << 6)
if flags[3] == 'r':
mode |= (4 << 3)
if flags[4] == 'w':
mode |= (2 << 3)
if flags[5] == 'x':
mode |= (1 << 3)
if flags[6] == 'r':
mode |= 4
if flags[7] == 'w':
mode |= 2
if flags[8] == 'x':
mode |= 1
return mode
def onRefresh(self, event):
"""Performs a refresh of the listing on the current remote directory."""
node = self.cache.findNode(self.getDir())
if node is not None:
node.data = None
thread = self.getMainThread()
thread.list()
def clearCache(self, event=None):
"""Clears the remote directory cache."""
del self.cache
self.cache = cache.DirectoryCacheTree()
def getTrueFileSize(self, file):
"""Gets the real remote file size (not the beautified printed size) in bytes.
If the file cannot be found in the current listing, then None is returned."""
listing = self.getListing()
if listing:
for entry in listing:
entry = self.parseListEntry(entry)
if entry is not None and entry[0] == file:
return entry[4]
return None
def getListing(self):
"""Gets the listing for the current directory.
An attempt is made to retrieve the listing from the cache. If the listing does
not exist in the cache, then a remote listing is executed."""
path_node = self.cache.findNode(self.getDir())
if path_node is None or path_node.data is None:
thread = self.getMainThread()
thread.list()
else:
self.setList(path_node.data)
def setList(self, list):
"""Sets the contents of the browser window to match the specified listing."""
if self.sorted:
list = self.sortList(list)
self.list.Freeze()
self.clearList()
for i in range(len(list)):
item = list[i]
img_index = self.selectBitmapIndex(item[3])
self.list.InsertImageStringItem(i, item[0], img_index)
self.list.SetStringItem(i, 1, item[1])
self.list.SetStringItem(i, 2, item[2])
self.list.SetStringItem(i, 3, item[3])
self.list.SetItemData(i, i)
self.list.Thaw()
def getList(self):
"""Gets the cotents of the browser window's current listing in python list form."""
list =[ ]
count = self.list.GetItemCount()
for i in range(count):
entry = [ ]
for j in range(4):
entry.append(utils.getColumnText(self.list, i, j))
list.append(tuple(entry))
return list
def updateListing(self, dir):
"""Updates the listing for the specified directory.
Special handling is provided for the '.' and '..' entries so they work as
expected."""
if dir == '.':
pass
else:
if dir =='..':
dir = self.join([ dir, self.getDir() ])
self.changeDirectory(dir, self.getDir())
self.getListing()
def displayFreshListing(self, listing):
"""Updates the list to display the result of a fresh listing from a remote file
tranfer site."""
listitems = [ ]
cache_node = self.cache.addNode(self.getDir())
for entry in listing:
parsed = self.parseListEntry(entry)
if parsed is None:
continue
file, truesize, date, flags = parsed
# Make the size more human readable
size = utils.beautifySize(truesize)
# Check if the file is a link and if so, clean up the file name
if flags and flags[0] == 'l':
match = self.LINK_RE.match(file)
if match is not None:
file, dest = match.groups()
else:
# Try the broken link match
match = self.BROKEN_LINK_RE.match(file)
if match is not None:
file = match.group(1)
# Extract the file
index = file.rfind(self.REMOTE_SEP) + 1
file = file[index:]
else:
if __debug__:
print "WARNING: Match failed for %s" %file
listitems.append((file, size, date, flags, str(truesize)))
# Check if the server provided us with a '..' entry. Otherwise, make one
found = [ i for i in listitems if i[0] == '..' ]
if not found:
match = self.TIME_RE.match(time.ctime(time.time()))
localtime = match.group(1)
dotdotentry = ( '..', _("0 Bytes"), localtime, '', '0')
listitems.insert(0, dotdotentry)
# Keep the listing around in case we need it
cache_node.data = listitems
self.setList(listitems)
def parseListEntry(self, entry):
"""Parses a remote file listing.
Returns a tuple containg the (file, size, date, flags) of the remote listing entry."""
result = None
# Try the usual unix matching
match = self.LIST_RE.match(entry)
if match:
flags, hardlinks, user, group, size, date, file = match.groups()
result = (file, long(size), date, flags)
# Try the MS ftp list (UNIX-like) matching
match = self.MSWIN_LIST_RE.match(entry)
if match:
flags, group, user, size, date, file = match.groups()
result = (file, long(size), date, flags)
# Try the MS ftp list (DOS) matching
match = self.MSDOS_LIST_RE.match(entry)
if match:
date, flags, size, file = match.groups()
result = (file, long(size or 0), date, (flags or '-').lower())
if __debug__:
if result is None:
print "WARNING: listing match failed for [%s]" %entry
return result
def sortList(self, list):
"""Performs sorting of the remote listing."""
sorted = [ ]
if self.directories_first:
dirs = [ i for i in list if not i[3] or i[3][0] == 'd' ]
links = [ i for i in list if i[3] and i[3][0] == 'l' ]
files = [ i for i in list if i[3] and i[3][0] in 'f-' ]
sorted.extend(self.performSort(dirs))
sorted.extend(self.performSort(links))
sorted.extend(self.performSort(files))
else:
sorted.extend(self.performSort(list))
return sorted
class CwdErrorHandler(dispatcher.ErrorHandler):
"""Change of working directory error handler.
This error handler is called when an attempt to change a working directory fails. This
results in a change back to the previous directory."""
def __init__(self, dir, remote_win, logger):
"""Creates an error handler where 'dir' points to the old directory."""
dispatcher.ErrorHandler.__init__(self)
self.dir = dir
self.remote_win = remote_win
self.logger = logger
def handle(self, error, dispatcher):
"""Handles a remote error by setting the current working directory back to the old
directory specified at the creation of the handler."""
if self.logger:
self.logger.log(Logger.ERROR, error)
self.remote_win.updateStatusBar(self.dir)
self.remote_win.setDir(self.dir)
|