|
@@ -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"
|