Samstag, 24. Juli 2010

Nautilus-Erweiterung mit Python

Heute gab es bei mir ja schon eine Explorer-Erweiterung mit Python. Das gleiche benötige ich auch am heimischen Rechner - nur unter Nautilus:
#!/usr/bin/python
# -*- coding: utf-8 -*-

import urllib
import easygui
import logging
import nautilus
import os

CCDFOLDERS =  [
 'source',
 'bin',
 'build',
 'lib',
 'resource',
 ]

class CcdDirStructureExtension(nautilus.MenuProvider):

 def __init__(self):
  pass
 
 
 def _alertError(self, message):
  easygui.msgbox(message, title='Alert', ok_button='Mist!')
 
 
 def _create_ccd_structure(self, foldername):
  ok = easygui.ynbox(msg=u'wirklich eine CCD-Struktur ersellen in\n' + foldername,
   title=u'Erstellen bestägen', choices=('Ja, bitte', 'Nein, danke'))
  if ok == 0:
   return

  if  not os.path.isdir(foldername):
   self._alertError(foldername + u'\nist kein Verzeichnis. Abgebrochen.')
   return

  try:
   for ccd in CCDFOLDERS:
    dir = os.path.join(foldername, ccd)
    os.mkdir(dir)
  except Exception, e:
   self._alertError(e)
   raise
 
 
 def menu_activate(self, menu, file):
  """Called when the user selects the menu."""
  filename = urllib.unquote(file.get_uri()[7:])
  self._create_ccd_structure(filename)


 def get_file_items(self, window, files):
  """Called when the user selects a file in Nautilus."""  
  if len(files) != 1:
   return
   
  file = files[0]
  if not file.is_directory() or file.get_uri_scheme() != 'file':
   return

  return self._get_MenuItemForFile(file)
  

 def get_background_items(self, window, file):
  """Called when the user clicks the background of a Nautilus-Window"""
  if not file.is_directory() or file.get_uri_scheme() != 'file':
   return
  return self._get_MenuItemForFile(file)


 def _get_MenuItemForFile(self, file):
  item = nautilus.MenuItem("NautilusPython::ccd-dir::create",
   "CCD-Struktur", 
   "erstellt eine CCD-Verzeichnisstruktur in %s" % file.get_name())
  item.connect("activate", self.menu_activate, file)
  return item,


if __name__ == '__main__':
 print 'This is a Nautilus extenion. Copy it to ${HOME}/.nautilus/python-extensions/ and restart Nautilus. Have fun.'
Der Code gehört bei Installierten Nautilus-Python-Bindings nach ${HOME}/.nautilus/python‑extensions/ und sollte ausführbar sein (chmod 755).
Die Dialoge werden von EasyGui erstellt, irgendwie gab es Probleme gtk-Dialoge zu erzeugen...

Explorer-Erweiterung (shell extension) mit Python

Die Problemstellung: Eine Verzeichnisstruktur soll automatisch erstellt werden, und zwar über einen Eintrag im Context-Menü des Explorers.
Die ganze Vorgeschichte ist hier.
Nun die Lösung in Python. Das ganze ist recht übersichtlich:

# -*- coding: utf-8 -*-

#   Erstellt einen Eintrag im Context-Menü des Explorers: CCD-Verzeichnis erstellen
#   bei Click wird im gewählten Verzeichnis eine CCD-Verzeichnisstrukur
#   (bin, build, lib, source, resources) erstellt.

import os.path
import pythoncom
from win32com.shell import shell, shellcon
import win32gui
import win32con

IContextMenu_Methods = ["QueryContextMenu", "InvokeCommand", "GetCommandString"]
IShellExtInit_Methods = ["Initialize"]

#   HKCR Key   Affected object types
#      *    All files
#AllFileSystemObjects  All regular files and file folders
#     Folder   All folders, virtual and filesystem
#     Directory   File folders
#Directory\Background  Directory-Background (Folder is open, one clicks on the white background...)
#      Drive   Root folders of all system drives
#    Network   Entire network
#   NetShare   All network shares

TYPES = [
 'Directory\\Background',
 'Directory',
 ]
SUBKEY = 'CCD-Verzeichnis'

CCDFOLDERS = [
 'source',
 'bin',
 'build',
 'lib',
 'resource',
 ]

def alertError(hwnd, exc):
 win32gui.MessageBox(hwnd, str(exc), str(exc.__class__), win32con.MB_OK)


class ShellExtension:
 _reg_progid_ = "CCD.Verzeichnisersteller.ShellExtension.ContextMenu"
 _reg_desc_ = "CCD-Verzeichnis Shell Extension (context menu)"
 _reg_clsid_ = "{5C664DC4-5ADA-4385-9DEB-EDB51320A668}"

 _com_interfaces_ = [shell.IID_IShellExtInit, shell.IID_IContextMenu]
 _public_methods_ = IContextMenu_Methods + IShellExtInit_Methods

 def Initialize(self, folder, dataobj, hkey):
  self.dataobj = dataobj

 def QueryContextMenu(self, hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags):
  try:
   msg = 'CCD-Verzeichnis ersellen'

   idCmd = idCmdFirst
   items = []
   if (uFlags & 0x000F) == shellcon.CMF_NORMAL:
    items.append(msg)
   elif uFlags & shellcon.CMF_VERBSONLY:
    items.append(msg)
   elif uFlags & shellcon.CMF_EXPLORE:
    items.append(msg)
   else:
    pass
    
   win32gui.InsertMenu(hMenu, indexMenu,
        win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
        0, None)
   indexMenu += 1
   for item in items:
    win32gui.InsertMenu(hMenu, indexMenu,
         win32con.MF_STRING|win32con.MF_BYPOSITION,
         idCmd, item)
    indexMenu += 1
    idCmd += 1

   win32gui.InsertMenu(hMenu, indexMenu,
        win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
        0, None)
   indexMenu += 1
   return idCmd-idCmdFirst

  except Exception, e:
   alertError(None, e)
   raise


 def InvokeCommand(self, ci):
  mask, hwnd, verb, params, dir, nShow, hotkey, hicon = ci
  
  try:
   if self.dataobj is None:
    #background-click
    fname = dir
   else:
    #get Files from dragObject
    files = self.getDragFiles()
    if not files:
     return
    fname = files[0]

   self.CreateCCDFolderStructure(hwnd, fname)
  except Exception, e:
   alertError(hwnd, e)
   raise
  return


 def GetCommandString(self, cmd, typ):
  return "Erstellt eine CCD-Verzeichnisstruktur"


 def getDragFiles(self):
  # Format the DataObject using a formatec, then get DragQueryFile from it...
  format_etc = win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL
  sm = self.dataobj.GetData(format_etc)
  num_files = shell.DragQueryFile(sm.data_handle, -1)
  files = [shell.DragQueryFile(sm.data_handle, i) for i in range(num_files)]
  return files
  
  
 def CreateCCDFolderStructure(self, hwnd, folder):
  ok = win32gui.MessageBox(hwnd, u'wirklich eine CCD-Struktur ersellen in\n' + folder,
   u'Erstellen bestägen', 
   win32con.MB_YESNO|win32con.MB_ICONQUESTION|win32con.MB_TASKMODAL|win32con.MB_SETFOREGROUND)
  if ok != win32con.IDYES:
   return
  
  if not os.path.isdir(folder):
   alertError(hwnd, folder + u'\nist kein Verzeichnis. Abgebrochen.')
  
  try:
   for ccd in CCDFOLDERS:
    dir = os.path.join(folder, ccd)
    os.mkdir(dir)
  except Exception, e:
   alertError(hwnd, e)
   raise
  return 

def DllRegisterServer():
 import _winreg
 for typ in TYPES:
  key = _winreg.CreateKey(_winreg.HKEY_CLASSES_ROOT, "%s\\shellex" % typ)
  subkey = _winreg.CreateKey(key, "ContextMenuHandlers")
  subkey2 = _winreg.CreateKey(subkey, SUBKEY)
  _winreg.SetValueEx(subkey2, None, 0, _winreg.REG_SZ, ShellExtension._reg_clsid_)
 print ShellExtension._reg_desc_, "registration complete."


def DllUnregisterServer():
 import _winreg
 for typ in TYPES:
  try:
   key = _winreg.DeleteKey(_winreg.HKEY_CLASSES_ROOT, "%s\\shellex\\ContextMenuHandlers\\%s" % (typ, SUBKEY))
  except WindowsError, details:
   import errno
   if details.errno != errno.ENOENT:
    raise
 print ShellExtension._reg_desc_, "unregistration complete."

 
def main(argv):
 from win32com.server import register
 register.UseCommandLine(ShellExtension,
       finalize_register = DllRegisterServer,
       finalize_unregister = DllUnregisterServer)

if __name__=='__main__':
 import sys
 main(sys.argv)

Für die Verwendung muss Python installiert sein (Empfehlung: 2.6) und passend zu der Python-Version PyWin32 (Empfehlung: 214.win32-py2.6)

Die Python-Datei in ein „kluges“ Verzeichnis legen (z.B. %ProgramFiles%\ccd‑verzeichnis\) und in der registry registrieren per doppelklick, oder über die Eingabeaufforderung mit ccd-verzeichnis.py --register. Wenn das ganze wieder entfernt werden soll, so geht dies nur über die Eingabeaufforderung mit ccd-verzeichnis.py --unregister.

Viel Spass.

Edits:
- Das ganze gibt's natürlich auch als Nautilus-Erweiterung...
- Vorlage für den Code war eine Beispiel-Anwendung des PyWin32-Projektes

CCD-Projekt Verzeichnisstruktur automatisiert erstellen

Im „Clean Code Developer Camp“ bekam ich die Empfehlung, dass die Verzeichnisstruktur in einem Projekt (Entwicklungsverzeichnisse, keine Abrechnungen, Verträge oder so...) einheitlich sein sollte. Die Empfehlung war unter dem Toplevel die Folgenden Unterverzeichnisse:
  • bin - Alles, was erstellt wird (start.exe, ergänzung.dll, …) landet hier.
  • build - Hier findet sich das Buildskript um das Projekt komplett zu bauen
  • lib - 3rd-Party-Tools finden sich hier.
  • resources - Hier findet sich alles, was sonst noch benötigt wird um das fertige Projekt laufen zu lassen
  • source - Die Sourcen...
Da ich das Problem habe mir das nicht immer merken zu können und außerdem eine manuelle Anlage dieser Struktur immer Zeit kostet habe ich mir gedacht ich schreibe einfach mal eine Erweiterung für den Explorer, mit der Funktion diese Struktur in einem Verzeichnis zu erstellen.

Für den durchschnittlichen nicht-c-Programmierer ist das aber alles andere als einfach habe ich herausfinden müssen.  
Meine Erste Idee war c#, mit P/Invoke... Das hat bisher nicht funktioniert wenn ich das zum Laufen bekomme, werde ich darüber berichten.
Der aktuelle Ansatz ist Python, mit PyWin32. Das scheint recht simpel.

Donnerstag, 22. Juli 2010

Dateinamen bereinigen

Meine Problemstellung gerade: Diverse Dateien in diversen Unterverzeichnissen haben "komische" Dateinamen und lassen sich unter Windows nicht entpacken...

Mein quick-Fix: Dateinamen bereinigen (natürlich unter Linux...) und alle nicht Standard Zeichen ersetzen.
In diesem Fall war es für mich ausreichend Buchstaben, Zahlen und den Punkt zu lassen, der Rest wurde durch Unterstriche ersetzt

Dafür verwendete ich folgendes Skript:
find -depth -name "* *" | while read file; do
   filePath="$(dirname "${file}")"
   oldFileName="$(basename "${file}")"
   newFileName=${oldFileName//[^a-zA-Z0-9\.]/_}
   mv "${filePath}"/"${oldFileName}" "${filePath}"/"${newFileName}"
done

Das ganze hat nur noch ein kleines Problem: Es funktioniert nicht für Dateinamen mit Leerzeichen am Ende des Dateinamens - hier muss das Skript verbessert werden, oder manuell nachgearbeitet.

Samstag, 17. Juli 2010

bash Autovervollständigung die Zweite

Gestern habe ich über die Autovervollständigung der bash geschrieben. Heute möchte ich kurz beschreiben wie ich das Verhalten verbessert habe.
Die noch fehlenden Funktionen waren:
  1. Die Autovervollständigung von Verzeichnisnamen
  2. Die Autovervollständigung von csv-Dateien in anderen Verzeichnissen als dem aktuellen.
Das Skript habe ich nun folgendermaßen abgeändert:
_csv2qif_possible_csvFiles()
{
   local curWord csvs optfiles
   curWord=${1}
   optfiles=""
   csvs=$( compgen -f -X "!*.csv" -- "${curWord}" )
   for file in ${csvs}; do
      if [ ! -f ${file/\.csv/.qif} ]; then
         optfiles="${optfiles} ${file}"
      fi
   done
   echo $optfiles
}

_csv2qif()
{
   local cur opts
   cur="${COMP_WORDS[COMP_CWORD]}"
   opts=$(_csv2qif_possible_csvFiles ${cur})

   COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
   return 0
}

complete -o plusdirs -F _csv2qif csv2qif.py

In Zeile 25 wird die Funktion Nr. 2 (Autovervollständigung von Verzeichnisnamen) realisiert. Durch den Parameter -o plusdirs wird die Liste der Autovervollständigung um die möglichen Verzeichnisse erweitert.

In Zeile 6 wird die Funktion Nr. 1 (andere Verzeichnisse als das aktuelle) realisiert. Die bash „hat da schon etwas vorbereitet“: compgen -f -X "!*.csv" -- "${curWord}" Stellt eine Liste für die Autovervollständigung zusammen. Und zwar werden hier für den Parameter ${curWord} alle passenden Dateien (-f) mit der Endung .csv (-X "!*.csv") Aufgelistet. Dabei wird schon bedacht, dass der Parameter auf ein anderes Verzeichnis zeigen könnte...

Finde ich super.
Dieses Mal habe ich meine Inspiration in der bash-Referenz gefunden, unter Programmable-Completion.

Freitag, 16. Juli 2010

bash Autovervollständigung

Für ein bestimmtes Konto exportiere ich die Umsatzdaten aus dem Web-Frontend meiner Bank. Diese (merkwürdige) csv-Datei wandle ich dann in qif um, da GnuCash kein csv importiert (den verwendeten Konverter findet man im Archiv der GnuCash-De liste).
Da ich die cvs-Dateien und die qif-Dateien nebeneinander archiviere habe ich manchmal beim konvertieren das Problem, das ich nicht mehr so genau weiß, was es nun zu konvertieren gilt (bzw. welche csv-Dateien noch nicht konvertiert sind)....

Abhilfe habe ich mir von der programmierbaren Eingabe-Vervollständigung der Bash versprochen. Meine Idee: Für die Eingabe-Vervollständigung sollten nur csv-Dateien in Betracht kommen und hier auch nur diese, für die noch keine Datei mit Endung qif besteht. Gedacht, getippt:

_csv2qif_possible_csvFiles()
{
 local csvs optfiles
 optfiles=""
 csvs=$(find . -maxdepth 1 -iname "*.csv" | sed "s/\.\///g")
 for file in ${csvs}; do
  if [ ! -f ${file/\.csv/.qif} ]; then
   optfiles="${optfiles} ${file}"
  fi
 done
 echo $optfiles
}

_csv2qif()
{
 local cur opts
 cur="${COMP_WORDS[COMP_CWORD]}"
 opts=$(_csv2qif_possible_csvFiles)

 COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
 return 0
}

complete -F _csv2qif csv2qif.py

Diese Datei beim Starten der Bash mit einlesen oder bei gentoo einfach in /etc/.bash_completion.d/ oder ${HOME}/.bash_completion.d/ kopieren und schon klappt's auch mit der Eingabe-Vervollständigung.
Geholfen hat mir übrigens die Debian-Einführung zu bash completion.

Screenshots finde ich bei Kommandozeilen-Tools immer nicht so toll, hier ist Trotzdem einer:

Als mögliche Verbesserung bleibt noch die Suche der csv-Dateien nicht nur auf das aktuelle Verzeichnis zu beschränken, sondern einen eingegebenen pfad (z.B.: ../csv/2010/Q3/) mit zu beachten, aber das hebe ich mir wohl mal für einen „Urlaub“ auf...

Edit: Eine verbesserte Version findet sich in: bash Autovervollstandigung die Zweite.

Dienstag, 6. Juli 2010

Webspace-Backup...

Mir ist heute aufgefallen, dass die Sicherung meines webspace nicht so war wie ich es mir gewünscht hätte...

Das Problem war das löschen der nicht mehr benötigten Dateien. (Dinge, die nicht mehr auf dem Server sind brauche ich auch nicht in der Sicherung...)
Bisher hatte ich "wget --mirror ..." auf einem cron-job, nur leider schaufelt wget immer nur Daten dazu, löscht aber keine alten Daten.
 
Nach etwas Suchen habe ich das Backup nun auf lftp umgestellt. Dazu habe ich auch gleich eine Mail-Benachrichtigung im Fehlerfall realisiert.

Folgendes Skript läuft nun täglich:

#!/bin/bash
pushd $(dirname $0) > /dev/null

lftp -c "set ftp:list-options -a;
open -u user,pass ftp://server.dom.de;
lcd backup-dir;
mirror \
       --use-cache \
       --delete \
       --allow-chown \
       --allow-suid \
       --no-umask \
       --parallel=2 \
       --exclude .listing \
       --exclude .configs \
       --exclude atd \
       --log=../mirror.log" \
 2> error.log \
 > /dev/null

if [ "$?" -ne 0 ]; then
   cat > last.mail.eml <<EOF
Subject: BACKUP nils-andresen.de FEHLGESCHLAGEN !!!
X-Priority: 1
Importance: High

 ---- ERROR-LOG: ----
EOF
   cat error.log >> last.mail.eml
   cat >> last.mail.eml <<EOF

 ---- MIROR-LOG: ----
EOF
   cat mirror.log >> last.mail.eml
   /usr/sbin/sendmail -i nils < last.mail.eml
fi

popd > /dev/null

Die Mail wird nur „intern“ (an den Benutzer nils) versendet, für mich ist das aber ausreichend...