#!/usr/bin/python """ a simple gui or command line app to view and create/edit file comments comments are stored in xattr user.xdg.comment depends on python-pyxattr because files are so often over-written, save a copy of the comments in a database ~/.dirnotes.db these comments stick to the symlink """ import sys,os,argparse from dirWidget2 import DirWidget from Tkinter import * from ttk import * import xattr, sqlite3, time VERSION = "0.2" COMMENT_KEY = "user.xdg.comment" DATABASE_NAME = "~/.dirnotes.db" # convert the ~/ form to a fully qualified path DATABASE_NAME = os.path.expanduser(DATABASE_NAME) class DataBase: ''' the database is flat fileName: fully qualified name st_mtime: a float size: a long comment: a string comment_time: a float, the time of the comment save this is effectively a log file, as well as a resource for a restore (in case a file-move is done without comment) the database is associated with a user, in the $HOME dir ''' def __init__(self): '''try to open the database; if not found, create it''' try: self.db = sqlite3.connect(DATABASE_NAME) except sqlite3.OperationalError: print("Database %s not found" % DATABASE_NAME) raise OperationalError c = self.db.cursor() try: c.execute("select * from dirnotes") except sqlite3.OperationalError: print("Table %s created" % ("dirnotes")) c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME)") def getData(self, fileName): c = self.db.cursor() c.execute("select * from dirnotes where name=? order by comment_date desc",(os.path.abspath(fileName),)) return c.fetchone() def setData(self, fileName, _date, _size, comment): c = self.db.cursor() c.execute("insert into dirnotes values (?,?,?,?,?)", (fileName, _date, _size, comment, time.time())) self.db.commit() return True def log(self, fileName, comment): ''' TODO: convert filename to canonical ''' c = self.db.cursor() s = os.stat(fileName) print ("params: %s %f %d %s %f" % ((os.path.abspath(fileName), s.st_mtime, s.st_size, comment, time.time()))) c.execute("insert into dirnotes values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'))", (os.path.abspath(fileName), s.st_mtime, s.st_size, str(comment), time.time())) self.db.commit() def parse(): parser = argparse.ArgumentParser(description='dirnotes application') parser.add_argument('dirname',metavar='dirname',type=str, help='directory [default=current dir]',default=".",nargs='?') parser.add_argument('dirname2',help='comparison directory, shows two-dirs side-by-side',nargs='?') parser.add_argument('-n','--nogui',action="store_const",const="1", help='use text base interface') parser.add_argument('-v','--version',action='version',version='%(prog)s '+VERSION) group = parser.add_mutually_exclusive_group() group.add_argument('-s','--sort-by-name',metavar='sort',action="store_const",const='n') group.add_argument('-m','--sort-by-date',metavar='sort',action='store_const',const='d') return parser.parse_args() class FileObj(): def __init__(self, fileName): self.fileName = fileName # also get the date, directory or not, etc self.comment = '' try: self.comment = xattr.getxattr(fileName,COMMENT_KEY) except Exception as e: #print("comment read on %s failed, execption %s" % (self.fileName,e)) pass def getName(self): return self.fileName def getShortName(self): if self.fileName[-1]=='/': #do show dirs, they can have comments return os.path.basename(self.fileName[:-1])+'/' else: return os.path.basename(self.fileName) def getComment(self): return self.comment def setComment(self,newComment): self.comment = newComment try: xattr.setxattr(self.fileName,COMMENT_KEY,self.comment) return True # we need to move these cases out to a handler except Exception as e: print("problem setting the comment on file %s" % (self.fileName,)) if os.access(self.fileName, os.W_OK)!=True: print("you don't appear to have write permissions on this file") # change the listbox background to yellow self.displayBox.notifyUnchanged() elif "Errno 95" in str(e): print("is this a VFAT or EXFAT volume? these don't allow comments") return False class DirNotes(Frame): ''' the main window of the app has 3 list boxes: dir_left, dir_right (may be invisible) and files ''' def __init__(self, parent, filename, db): Frame.__init__(self,parent) self.db = db self.lb = lb = Treeview(self) lb['columns'] = ('comment') lb.heading('#0',text='Name') lb.heading('comment',text='Comment') # resize the comments column # and resize the parent window to match the directory size # allow multiple entries on the line at this point #d = os.listdir(p.filename[0]) #d.sort() current, dirs, files = os.walk(filename,followlinks=True).next() dirs = map(lambda x:x+'/', dirs) dirs.sort() files.sort() d = dirs + files self.files = [] for i in range(len(d)): this_file = FileObj(current+'/'+d[i]) self.files = self.files + [this_file] lb.insert('','end',iid=str(i),text=this_file.getShortName(),) #lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags) comment = this_file.getComment() lb.set(item=str(i),column='comment',value=comment) e2 = Label(self,text="View and edit file comments stored in extended attributes user.xdg.comment") e1 = Label(self,text="Active Directory:") b1 = Button(self,text="restore from database") dirLeft = DirWidget(self,current) #dirLeft.setMaximumHeight(140) #dirLeft.setMaximumWidth(200) dirRight = DirWidget(self,current) #~ dirRight.setMaximumHeight(140) #~ dirRight.setMaximumWidth(200) #~ dirRight.setEnabled(False) #~ layout = QVBoxLayout() #~ upperLayout = QHBoxLayout() #~ layout.addWidget(e) #~ upperLayout.addWidget(dirLeft) #~ upperLayout.addWidget(b1) #~ upperLayout.addWidget(dirRight) #~ layout.addLayout(upperLayout) #~ layout.addWidget(lb) #~ win.setLayout(layout) #~ lb.itemChanged.connect(self.change) #~ b1.pressed.connect(self.restore_from_database) #~ QShortcut(QKeySequence("Ctrl+Q"), self, self.close) #~ self.setWindowTitle("test") #~ self.setMinimumSize(600,400) e1.pack(anchor=W,padx=20) dirLeft.pack(anchor=W,padx=20,pady=5) e2.pack() lb.pack() def closeEvent(self,e): print("closing") def change(self,x): print("debugging " + x.text() + " r:" + str(x.row()) + " c:" + str(x.column())) the_file = dn.files[x.row()] r = the_file.setComment(str(x.text())) if r: self.db.log(the_file.getName(),x.text()) def restore_from_database(self): print("restore from database") fileName = str(self.lb.item(self.lb.currentRow(),0).text()) fo_row = self.db.getData(fileName) if len(fo_row)>1: comment = fo_row[3] print(fileName,fo_row[0],comment) if __name__=="__main__": p = parse() if p.dirname[-1]=='/': p.dirname = p.dirname[:-1] print(p.dirname) db = DataBase() tk_basis = Tk() tk_basis.title("DirNotes "+p.dirname) dn = DirNotes(tk_basis,p.dirname,db) dn.pack() mainloop() #xattr.setxattr(filename,COMMENT_KEY,commentText) ''' files from directories use os.isfile() os.isdir() current, dirs, files = os.walk("path").next() possible set folllowLinks=True''' ''' notes from the wdrm project table showed filename, size, date size, date, desc at start, fills the list of all the files skip the . entry ''' ''' should we also do user.xdg.tags="TagA,TagB" ? user.charset user.creator=application_name or user.xdg.creator user.xdg.origin.url user.xdg.language=[RFC3066/ISO639] user.xdg.publisher ''' ''' to allow column-sorting, you use the sortByColumn and set the Horiz-header to clickable ''' ''' TODO: also need a way to display-&-restore comments from the database ''' ''' QFileDialog -make my own? -existing one has -history -back button -up button -but we don't need -directory date -icon option -url browser (unless we go network file system) -new folder button -file type chooser -text entry box -choose & cancel buttons ''' ''' http://stackoverflow.com/questions/18562123/how-to-make-ttk-treeviews-rows-editable '''