|
@@ -1,273 +0,0 @@
|
|
|
-#!/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
|
|
|
-'''
|
|
|
-
|