123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- #!/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,stat
- from dirWidget import DirWidget
- from PyQt4.QtGui import *
- from PyQt4 import QtGui, QtCore
- 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=? and comment<>'' 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 %s %d %s %s" % ((os.path.abspath(fileName),
- DataBase.epochToDb(s.st_mtime), s.st_size, comment,
- DataBase.epochToDb(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()
- @staticmethod
- def epochToDb(epoch):
- return time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(epoch))
- @staticmethod
- def DbToEpoch(dbTime):
- return time.mktime(time.strptime(dbTime,"%Y-%m-%d %H:%M:%S"))
-
- 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
- s = os.stat(fileName)
- self.date = DataBase.epochToDb(s.st_mtime)
- if stat.S_ISDIR(s.st_mode):
- self.size = -1
- else:
- self.size = s.st_size
- 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 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
- def getDate(self):
- return self.date
- def getSize(self):
- return self.size
- class DirNotes(QMainWindow):
- ''' the main window of the app
- has 3 list boxes: dir_left, dir_right (may be invisible) and files
-
- '''
- def __init__(self, argFilename, db, parent=None):
- super(DirNotes,self).__init__(parent)
- self.db = db
- win = QWidget()
- self.setCentralWidget(win)
- lb = QTableWidget()
- self.lb = lb
- lb.setColumnCount(4)
- lb.horizontalHeader().setResizeMode( 1, QHeaderView.Stretch );
- lb.verticalHeader().setDefaultSectionSize(20); # thinner rows
- lb.verticalHeader().setVisible(False)
-
- # 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()
- self.curPath, filename = os.path.split(argFilename)
- print("working on <"+self.curPath+"> and <"+filename+">")
-
- self.refill()
-
- lb.setHorizontalHeaderItem(0,QTableWidgetItem("File"))
- lb.setHorizontalHeaderItem(1,QTableWidgetItem("Date/Time"))
- lb.setHorizontalHeaderItem(2,QTableWidgetItem("Size"))
- lb.setHorizontalHeaderItem(3,QTableWidgetItem("Comment"))
- lb.resizeColumnsToContents()
- e = QLabel("View and edit file comments stored in extended attributes user.xdg.comment",win)
- b1 = QPushButton("restore from database",win)
- self.dirLeft = dirLeft = DirWidget(self.curPath,win)
- dirLeft.setMaximumHeight(140)
- dirLeft.setMaximumWidth(200)
- dirRight = DirWidget(self.curPath,win)
- dirRight.setMaximumHeight(140)
- dirRight.setMaximumWidth(200)
- dirRight.setEnabled(False)
- dirLeft.selected.connect(self.newDir)
-
- rDate = QRadioButton("Sort by date",win)
- rSize = QRadioButton("Sort by size",win)
-
- layout = QVBoxLayout()
- upperLayout = QHBoxLayout()
- innerLayout = QVBoxLayout()
- layout.addWidget(e)
- upperLayout.addWidget(dirLeft)
- innerLayout.addWidget(rDate)
- innerLayout.addWidget(rSize)
- innerLayout.addWidget(b1)
- upperLayout.addLayout(innerLayout)
- upperLayout.addWidget(dirRight)
- layout.addLayout(upperLayout)
- layout.addWidget(lb)
- win.setLayout(layout)
-
- lb.itemChanged.connect(self.change)
- b1.pressed.connect(self.restore_from_database)
- mb = self.menuBar()
- mf = mb.addMenu('&File')
- mf.addAction("Sort by name", self.sbn, "Ctrl+N")
- mf.addAction("Sort by date", self.sbd, "Ctrl+D")
- mf.addAction("Sort by size", self.sbs, "Ctrl+Z")
- mf.addAction("Sort by comment", self.sbc, "Ctrl+.")
- mf.addAction("Restore comment from database", self.restore_from_database, "Ctrl+R")
- QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
- self.setWindowTitle("DirNotes")
- self.setMinimumSize(600,700)
- lb.setFocus()
- def closeEvent(self,e):
- print("closing")
- def sbd(self):
- print("sort by date")
- self.lb.sortItems(1,QtCore.Qt.DescendingOrder)
- def sbs(self):
- print("sort by size")
- self.lb.sortItems(2)
- def sbn(self):
- print("sort by name")
- self.lb.sortItems(0)
- def sbc(self):
- print("sort by comment")
- self.lb.sortItems(3,QtCore.Qt.DescendingOrder)
- def newDir(self):
- print("change dir to "+self.dirLeft.currentPath())
-
- class DnTableWidgetItem(QTableWidgetItem):
- def __init__(self, text, value):
- QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
- self.value = value
- def __lt__(self, other):
- return self.value < other.value
-
- def refill(self):
- small_font = QFont("",8)
- dirIcon = QIcon.fromTheme('folder')
- fileIcon = QIcon.fromTheme('text-x-generic')
- current, dirs, files = os.walk(self.curPath,followlinks=True).next()
- dirs = map(lambda x:x+'/', dirs)
- dirs.sort()
- files.sort()
-
- d = dirs + files
- self.lb.setRowCount(len(d))
- self.files = []
- for i in range(len(d)):
- this_file = FileObj(current+'/'+d[i])
- self.files = self.files + [this_file]
- item = QTableWidgetItem(d[i])
- item.setFlags(QtCore.Qt.ItemIsEnabled)
- self.lb.setItem(i,0,item)
- #lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags)
- comment = this_file.getComment()
- self.lb.setItem(i,3,QTableWidgetItem(comment))
- da = QTableWidgetItem(this_file.getDate())
- da.setFont(small_font)
- da.setFlags(QtCore.Qt.ItemIsEnabled)
- self.lb.setItem(i,1,da)
- si = this_file.getSize()
- if si>=0:
- sa = self.DnTableWidgetItem(str(si),si)
- item.setIcon(fileIcon)
- else:
- sa = self.DnTableWidgetItem('',0)
- item.setIcon(dirIcon)
- sa.setTextAlignment(QtCore.Qt.AlignRight)
- sa.setFlags(QtCore.Qt.ItemIsEnabled)
- self.lb.setItem(i,2,sa)
-
- 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 fo_row and len(fo_row)>1:
- comment = fo_row[3]
- print(fileName,fo_row[0],comment)
- the_file = dn.files[self.lb.currentRow()]
- the_file.setComment(comment)
- self.lb.setItem(self.lb.currentRow(),3,QTableWidgetItem(comment))
-
- if __name__=="__main__":
- p = parse()
- if p.dirname[-1]=='/':
- p.dirname = p.dirname[:-1]
- if os.path.isdir(p.dirname):
- p.dirname = p.dirname + '/'
- print(p.dirname)
-
- db = DataBase()
- a = QApplication([])
- dn = DirNotes(p.dirname,db)
- dn.show()
-
- a.exec_()
-
- #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
-
- '''
|