|  | @@ -1,4 +1,9 @@
 | 
	
		
			
				|  |  |  #!/usr/bin/python3
 | 
	
		
			
				|  |  | +# TODO: get rid of sqlite cursors; execute on connection
 | 
	
		
			
				|  |  | +# TODO: add index to table creation
 | 
	
		
			
				|  |  | +# TODO: why are there TWO sqlite.connect()??
 | 
	
		
			
				|  |  | +#   try auto-commit (isolation_level = "IMMEDIATE")
 | 
	
		
			
				|  |  | +#   try dict-parameters in sql statements
 | 
	
		
			
				|  |  |  # TODO: pick up comment for cwd and display at the top somewhere, or maybe status line
 | 
	
		
			
				|  |  |  """ a simple gui or command line app
 | 
	
		
			
				|  |  |  to view and create/edit file comments
 | 
	
	
		
			
				|  | @@ -18,7 +23,7 @@ nav tools are enabled, so you can double-click to go into a dir
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  """
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -VERSION = "0.4"
 | 
	
		
			
				|  |  | +VERSION = "0.5"
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  helpMsg = f"""<table width=100%><tr><td><h1>Dirnotes</h1><td>
 | 
	
		
			
				|  |  |  <td align=right>Version: {VERSION}</td></tr></table>
 | 
	
	
		
			
				|  | @@ -52,8 +57,7 @@ media (as long as the disk format allows it).
 | 
	
		
			
				|  |  |  the comment box gets a light yellow background.
 | 
	
		
			
				|  |  |  """
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -import sys,os,argparse,stat,getpass
 | 
	
		
			
				|  |  | -#~ from dirWidget import DirWidget
 | 
	
		
			
				|  |  | +import sys,os,argparse,stat,getpass,shutil
 | 
	
		
			
				|  |  |  from PyQt5.QtGui import *
 | 
	
		
			
				|  |  |  from PyQt5.QtWidgets import *
 | 
	
		
			
				|  |  |  from PyQt5.QtCore import Qt, pyqtSignal
 | 
	
	
		
			
				|  | @@ -108,30 +112,16 @@ class dnDataBase:
 | 
	
		
			
				|  |  |      except sqlite3.OperationalError:
 | 
	
		
			
				|  |  |        print(f"Database {dbFile} not found")
 | 
	
		
			
				|  |  |        raise
 | 
	
		
			
				|  |  | -    self.db_cursor = self.db.cursor()
 | 
	
		
			
				|  |  |      try:
 | 
	
		
			
				|  |  | -      self.db_cursor.execute("select * from dirnotes")
 | 
	
		
			
				|  |  | +      self.db.execute("select * from dirnotes")
 | 
	
		
			
				|  |  |      except sqlite3.OperationalError:
 | 
	
		
			
				|  |  |        print_v("Table %s created" % ("dirnotes"))
 | 
	
		
			
				|  |  | -      self.db_cursor.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  | +      self.db.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
 | 
	
		
			
				|  |  | +      self.db_cursor.execute("create index dirnotes_i on dirnotes(name)") 
 | 
	
		
			
				|  |  | +  # getData is only used by the restore-from-database.......consider deleting it
 | 
	
		
			
				|  |  |    def getData(self, fileName):
 | 
	
		
			
				|  |  | -    self.db_cursor.execute("select * from dirnotes where name=? and comment<>'' order by comment_date desc",(os.path.abspath(fileName),))
 | 
	
		
			
				|  |  | -    return self.db_cursor.fetchone()
 | 
	
		
			
				|  |  | -  def setData(self, fileName, comment):
 | 
	
		
			
				|  |  | -    s = os.lstat(fileName)
 | 
	
		
			
				|  |  | -    print_v ("params: %s %s %d %s %s" % ((os.path.abspath(fileName), 
 | 
	
		
			
				|  |  | -        dnDataBase.epochToDb(s.st_mtime), s.st_size, comment, 
 | 
	
		
			
				|  |  | -        dnDataBase.epochToDb(time.time()))))
 | 
	
		
			
				|  |  | -    try:
 | 
	
		
			
				|  |  | -      self.db_cursor.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(), getpass.getuser()))
 | 
	
		
			
				|  |  | -      self.db.commit()
 | 
	
		
			
				|  |  | -    except sqlite3.OperationalError:
 | 
	
		
			
				|  |  | -      print("database is locked or unwriteable")
 | 
	
		
			
				|  |  | -      errorBox("database is locked or unwriteable")
 | 
	
		
			
				|  |  | -      #TODO: put up a message box for locked database
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +    c = self.db.execute("select * from dirnotes where name=? and comment<>'' order by comment_date desc",(os.path.abspath(fileName),))
 | 
	
		
			
				|  |  | +    return c.fetchone()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    @staticmethod
 | 
	
		
			
				|  |  |    def epochToDb(epoch):
 | 
	
	
		
			
				|  | @@ -157,13 +147,14 @@ class FileObj():
 | 
	
		
			
				|  |  |    FILE_IS_LINK   = -2
 | 
	
		
			
				|  |  |    FILE_IS_SOCKET = -3
 | 
	
		
			
				|  |  |    def __init__(self, fileName):
 | 
	
		
			
				|  |  | -    self.fileName = os.path.abspath(fileName)
 | 
	
		
			
				|  |  | -    self.displayName = os.path.split(fileName)[1]
 | 
	
		
			
				|  |  | +    self.fileName = os.path.abspath(fileName) # full path; dirs end WITHOUT a terminal /
 | 
	
		
			
				|  |  | +    self.displayName = os.path.split(fileName)[1]   # base name; dirs end with a /
 | 
	
		
			
				|  |  |      s = os.lstat(self.fileName)
 | 
	
		
			
				|  |  |      self.date = s.st_mtime
 | 
	
		
			
				|  |  |      if stat.S_ISDIR(s.st_mode):
 | 
	
		
			
				|  |  |        self.size = FileObj.FILE_IS_DIR
 | 
	
		
			
				|  |  | -      self.displayName += '/'
 | 
	
		
			
				|  |  | +      if not self.displayName.endswith('/'):
 | 
	
		
			
				|  |  | +        self.displayName += '/'
 | 
	
		
			
				|  |  |      elif stat.S_ISLNK(s.st_mode):
 | 
	
		
			
				|  |  |        self.size = FileObj.FILE_IS_LINK
 | 
	
		
			
				|  |  |      elif stat.S_ISSOCK(s.st_mode):
 | 
	
	
		
			
				|  | @@ -187,13 +178,13 @@ class FileObj():
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    def getName(self):
 | 
	
		
			
				|  |  |      return self.fileName
 | 
	
		
			
				|  |  | -  def getFileName(self):
 | 
	
		
			
				|  |  | +  def getDisplayName(self):
 | 
	
		
			
				|  |  |      return self.displayName
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    # with an already open database cursor
 | 
	
		
			
				|  |  | -  def loadDbComment(self,cursor):
 | 
	
		
			
				|  |  | -    cursor.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(self.fileName,))
 | 
	
		
			
				|  |  | -    a = cursor.fetchone()
 | 
	
		
			
				|  |  | +  def loadDbComment(self,db):
 | 
	
		
			
				|  |  | +    c = db.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(self.fileName,))
 | 
	
		
			
				|  |  | +    a = c.fetchone()
 | 
	
		
			
				|  |  |      if a:
 | 
	
		
			
				|  |  |        self.dbComment, self.dbAuthor, self.dbDate = a
 | 
	
		
			
				|  |  |        self.commentsDiffer = True if self.xattrComment == self.dbComment else False
 | 
	
	
		
			
				|  | @@ -204,19 +195,19 @@ class FileObj():
 | 
	
		
			
				|  |  |      return self.dbAuthor
 | 
	
		
			
				|  |  |    def getDbDate(self):
 | 
	
		
			
				|  |  |      return self.dbDate
 | 
	
		
			
				|  |  | -  def setDbComment(self,newComment):
 | 
	
		
			
				|  |  | -    try:
 | 
	
		
			
				|  |  | -      self.db = sqlite3.connect(dbName)
 | 
	
		
			
				|  |  | -    except sqlite3.OperationalError:
 | 
	
		
			
				|  |  | -      print_v(f"database {dbName} not found")
 | 
	
		
			
				|  |  | -      raise
 | 
	
		
			
				|  |  | -    c = self.db.cursor()
 | 
	
		
			
				|  |  | +  def setDbComment(self,db,newComment):
 | 
	
		
			
				|  |  |      s = os.lstat(self.fileName)
 | 
	
		
			
				|  |  |      try:
 | 
	
		
			
				|  |  | -      c.execute("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",
 | 
	
		
			
				|  |  | -          (os.path.abspath(self.fileName), s.st_mtime, s.st_size,
 | 
	
		
			
				|  |  | +      # TODO: copy from /g/test_file to /home/patb/project/dirnotes/r    fails on database.commit()
 | 
	
		
			
				|  |  | +      print_v(f"setDbComment db {db}, file: {self.fileName}")
 | 
	
		
			
				|  |  | +      print_v("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",
 | 
	
		
			
				|  |  | +          (self.fileName, s.st_mtime, s.st_size,
 | 
	
		
			
				|  |  |            str(newComment), time.time(), getpass.getuser()))
 | 
	
		
			
				|  |  | -      self.db.commit()
 | 
	
		
			
				|  |  | +      db.execute("insert into dirnotes (name,date,size,comment,comment_date,author) values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'),?)",
 | 
	
		
			
				|  |  | +          (self.fileName, s.st_mtime, s.st_size,
 | 
	
		
			
				|  |  | +          str(newComment), time.time(), getpass.getuser()))
 | 
	
		
			
				|  |  | +      print_v(f"setDbComment, execute done, about to commit()")
 | 
	
		
			
				|  |  | +      db.commit()
 | 
	
		
			
				|  |  |        print_v(f"database write for {self.fileName}")
 | 
	
		
			
				|  |  |        self.dbComment = newComment
 | 
	
		
			
				|  |  |      except sqlite3.OperationalError:
 | 
	
	
		
			
				|  | @@ -341,47 +332,6 @@ icon = ["32 32 6 1",  # the QPixmap constructor allows for str[]
 | 
	
		
			
				|  |  |  "  ...........................   ",
 | 
	
		
			
				|  |  |  "                                "]
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -''' a widget that shows only a dir listing
 | 
	
		
			
				|  |  | - '''
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -class DirWidget(QListWidget):
 | 
	
		
			
				|  |  | -  ''' a simple widget that shows a list of directories, staring
 | 
	
		
			
				|  |  | -  at the directory passed into the constructor
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  a mouse click or 'enter' key will send a 'selected' signal to
 | 
	
		
			
				|  |  | -  anyone who connects to this.
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  the .. parent directory is shown for all dirs except /
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  the most interesting parts of the interface are:
 | 
	
		
			
				|  |  | -  constructor - send in the directory to view
 | 
	
		
			
				|  |  | -  method - currentPath() returns text of current path
 | 
	
		
			
				|  |  | -  signal - selected calls a slot with a single arg: new path
 | 
	
		
			
				|  |  | -  '''
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  selected = pyqtSignal(str)
 | 
	
		
			
				|  |  | -  def __init__(self, directory='.', parent=None):
 | 
	
		
			
				|  |  | -    super(DirWidget,self).__init__(parent)
 | 
	
		
			
				|  |  | -    self.directory = directory
 | 
	
		
			
				|  |  | -    self.refill()
 | 
	
		
			
				|  |  | -    self.itemActivated.connect(self.selectionByLWI)
 | 
	
		
			
				|  |  | -    # it would be nice to pick up single-mouse-click for selection as well
 | 
	
		
			
				|  |  | -    # but that seems to be a system-preferences global
 | 
	
		
			
				|  |  | -  def selectionByLWI(self, li):
 | 
	
		
			
				|  |  | -    self.directory = os.path.abspath(self.directory + '/' + str(li.text()))
 | 
	
		
			
				|  |  | -    self.refill()
 | 
	
		
			
				|  |  | -    self.selected.emit(self.directory)
 | 
	
		
			
				|  |  | -  def refill(self):
 | 
	
		
			
				|  |  | -    current,dirs,files = next(os.walk(self.directory,followlinks=True))
 | 
	
		
			
				|  |  | -    dirs.sort()
 | 
	
		
			
				|  |  | -    if '/' not in dirs:
 | 
	
		
			
				|  |  | -      dirs = ['..'] + dirs
 | 
	
		
			
				|  |  | -    self.clear()
 | 
	
		
			
				|  |  | -    for d in dirs:
 | 
	
		
			
				|  |  | -      li = QListWidgetItem(d,self)
 | 
	
		
			
				|  |  | -  def currentPath(self):
 | 
	
		
			
				|  |  | -    return self.directory
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  # sortable TableWidgetItem, based on idea by Aledsandar
 | 
	
		
			
				|  |  |  # http://stackoverflow.com/questions/12673598/python-numerical-sorting-in-qtablewidget
 | 
	
	
		
			
				|  | @@ -457,7 +407,7 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  |      if len(filename)>0:
 | 
	
		
			
				|  |  |        for i in range(lb.rowCount()):
 | 
	
		
			
				|  |  | -        if filename == lb.item(i,0).data(32).getFileName():
 | 
	
		
			
				|  |  | +        if filename == lb.item(i,0).data(32).getDisplayName():
 | 
	
		
			
				|  |  |            lb.setCurrentCell(i,3)
 | 
	
		
			
				|  |  |            break
 | 
	
		
			
				|  |  |      
 | 
	
	
		
			
				|  | @@ -509,19 +459,16 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |      if not fo.isDir() and not fo.isLink():  # TODO: add check for socket 
 | 
	
		
			
				|  |  |        print_v(f"copy file {fo.getName()}")
 | 
	
		
			
				|  |  |        # open the dir.picker
 | 
	
		
			
				|  |  | -      # TODO: move all this out to a function
 | 
	
		
			
				|  |  | -      qd = QDialog(self.parent)
 | 
	
		
			
				|  |  | -      qd.setWindowTitle("Select destination for FileCopy")
 | 
	
		
			
				|  |  | -      qd.setWindowModality(Qt.ApplicationModal)
 | 
	
		
			
				|  |  | -      dw = DirWidget('.',qd)
 | 
	
		
			
				|  |  | -      d_ok = QPushButton('select')
 | 
	
		
			
				|  |  | -      d_ok.setDefault(True)
 | 
	
		
			
				|  |  | -      d_ok.clicked.connect(QDialog.accept)
 | 
	
		
			
				|  |  | -      d_nope = QPushButton('cancel')
 | 
	
		
			
				|  |  | -      d_nope.clicked.connect(QDialog.reject)
 | 
	
		
			
				|  |  | -      # if returns from <enter>, copy the file and comments
 | 
	
		
			
				|  |  | -      r = qd.exec() 
 | 
	
		
			
				|  |  | +      r = QFileDialog.getExistingDirectory(self.parent, "Select destination for FileCopy")
 | 
	
		
			
				|  |  |        print_v(f"copy to {r}")
 | 
	
		
			
				|  |  | +      if r:
 | 
	
		
			
				|  |  | +        dest = os.path.join(r,fo.getDisplayName())
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +          shutil.copy2(fo.getName(), dest) # copy2 preserves the xattr
 | 
	
		
			
				|  |  | +          f = FileObj(dest)       # can't make the FileObj until it exists
 | 
	
		
			
				|  |  | +          f.setDbComment(self.db,fo.getDbComment())
 | 
	
		
			
				|  |  | +        except:
 | 
	
		
			
				|  |  | +          errorBox(f"file copy to <{dest}> failed; check permissions")
 | 
	
		
			
				|  |  |      pass
 | 
	
		
			
				|  |  |        
 | 
	
		
			
				|  |  |    def refill(self):
 | 
	
	
		
			
				|  | @@ -550,13 +497,13 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |      # this is a list of all the file
 | 
	
		
			
				|  |  |      #~ print("insert {} items into cleared table {}".format(len(d),current))
 | 
	
		
			
				|  |  |      for i,name in enumerate(d):
 | 
	
		
			
				|  |  | -      this_file = FileObj(current+'/'+name)
 | 
	
		
			
				|  |  | -      this_file.loadDbComment(self.db.db_cursor)
 | 
	
		
			
				|  |  | +      this_file = FileObj(os.path.join(current,name))
 | 
	
		
			
				|  |  | +      this_file.loadDbComment(self.db)
 | 
	
		
			
				|  |  |        print_v("FileObj created as {} and the db-comment is <{}>".format(this_file.displayName, this_file.dbComment))
 | 
	
		
			
				|  |  |        #~ print("insert order check: {} {} {} {}".format(d[i],i,this_file.getName(),this_file.getDate()))
 | 
	
		
			
				|  |  |        #~ self.files.update({this_file.getName(),this_file})
 | 
	
		
			
				|  |  |        self.files = self.files + [this_file]
 | 
	
		
			
				|  |  | -      display_name = this_file.getFileName()
 | 
	
		
			
				|  |  | +      display_name = this_file.getDisplayName()
 | 
	
		
			
				|  |  |        if this_file.getSize() == FileObj.FILE_IS_DIR:
 | 
	
		
			
				|  |  |          item = SortableTableWidgetItem(display_name,' '+display_name, this_file)  # directories sort first
 | 
	
		
			
				|  |  |        else:
 | 
	
	
		
			
				|  | @@ -569,7 +516,7 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |        # get the comment from database & xattrs, either can fail
 | 
	
		
			
				|  |  |        comment = this_file.getComment()
 | 
	
		
			
				|  |  |        other_comment = this_file.getOtherComment()
 | 
	
		
			
				|  |  | -      ci = QTableWidgetItem(comment)
 | 
	
		
			
				|  |  | +      ci = SortableTableWidgetItem(comment,'',this_file)
 | 
	
		
			
				|  |  |        ci.setToolTip(f"comment: {comment}\ncomment date: {this_file.getDbDate()}\nauthor: {this_file.getDbAuthor()}")
 | 
	
		
			
				|  |  |        if other_comment != comment:
 | 
	
		
			
				|  |  |          ci.setBackground(QBrush(QColor(255,255,160)))
 | 
	
	
		
			
				|  | @@ -607,11 +554,10 @@ class DirNotes(QMainWindow):
 | 
	
		
			
				|  |  |      print_v("      selected file: "+self.lb.item(x.row(),0).file_object.getName())
 | 
	
		
			
				|  |  |      the_file = self.lb.item(x.row(),0).file_object
 | 
	
		
			
				|  |  |      print_v("      and the row file is "+the_file.getName())
 | 
	
		
			
				|  |  | -    the_file.setDbComment(str(x.text()))
 | 
	
		
			
				|  |  | +    the_file.setDbComment(self.db,str(x.text()))
 | 
	
		
			
				|  |  |      r = the_file.setXattrComment(str(x.text())) 
 | 
	
		
			
				|  |  | -    # TODO: change this to FileObj.setDbComment()
 | 
	
		
			
				|  |  |      if r:
 | 
	
		
			
				|  |  | -      self.db.setData(the_file.getName(),x.text())
 | 
	
		
			
				|  |  | +      the_file.setDbComment(self.db,x.text())
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    def switchMode(self):
 | 
	
		
			
				|  |  |      global mode
 | 
	
	
		
			
				|  | @@ -686,7 +632,7 @@ if __name__=="__main__":
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  |    print_v(f"here is the .json {repr(config)}")
 | 
	
		
			
				|  |  |    dbName = os.path.expanduser(config["database"])
 | 
	
		
			
				|  |  | -  db = dnDataBase(dbName)
 | 
	
		
			
				|  |  | +  db = dnDataBase(dbName).db
 | 
	
		
			
				|  |  |    xattr_comment = config["xattr_tag"]
 | 
	
		
			
				|  |  |    xattr_author  = xattr_comment + ".author"
 | 
	
		
			
				|  |  |    xattr_date    = xattr_comment + ".date"
 |