|  | @@ -0,0 +1,266 @@
 | 
	
		
			
				|  |  | +#!/usr/bin/python3
 | 
	
		
			
				|  |  | +""" 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 *
 | 
	
		
			
				|  |  | +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 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, filename, db, parent=None):
 | 
	
		
			
				|  |  | +		super(DirNotes,self).__init__(parent)
 | 
	
		
			
				|  |  | +		self.db = db
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		win = QWidget()
 | 
	
		
			
				|  |  | +		self.setCentralWidget(win)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		lb = QTableWidget()
 | 
	
		
			
				|  |  | +		self.lb = lb
 | 
	
		
			
				|  |  | +		lb.setColumnCount(2)
 | 
	
		
			
				|  |  | +		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()
 | 
	
		
			
				|  |  | +		current, dirs, files = os.walk(filename,followlinks=True).next()
 | 
	
		
			
				|  |  | +		dirs = map(lambda x:x+'/', dirs)
 | 
	
		
			
				|  |  | +		dirs.sort()
 | 
	
		
			
				|  |  | +		files.sort()
 | 
	
		
			
				|  |  | +		
 | 
	
		
			
				|  |  | +		d = dirs + files
 | 
	
		
			
				|  |  | +		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(this_file.getName())
 | 
	
		
			
				|  |  | +			item.setFlags(QtCore.Qt.ItemIsEnabled)
 | 
	
		
			
				|  |  | +			lb.setItem(i,0,item)
 | 
	
		
			
				|  |  | +			#lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags)
 | 
	
		
			
				|  |  | +			comment = this_file.getComment()
 | 
	
		
			
				|  |  | +			lb.setItem(i,1,QTableWidgetItem(comment))
 | 
	
		
			
				|  |  | +		lb.setHorizontalHeaderItem(0,QTableWidgetItem("file"))
 | 
	
		
			
				|  |  | +		lb.setHorizontalHeaderItem(1,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)
 | 
	
		
			
				|  |  | +		dirLeft = DirWidget(current,win)
 | 
	
		
			
				|  |  | +		dirLeft.setMaximumHeight(140)
 | 
	
		
			
				|  |  | +		dirLeft.setMaximumWidth(200)
 | 
	
		
			
				|  |  | +		dirRight = DirWidget(current,win)
 | 
	
		
			
				|  |  | +		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)
 | 
	
		
			
				|  |  | +	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()
 | 
	
		
			
				|  |  | +	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
 | 
	
		
			
				|  |  | +	
 | 
	
		
			
				|  |  | +	'''
 | 
	
		
			
				|  |  | +
 |