| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 | #!/usr/bin/python""" a simple gui or command line appto view and create/edit file commentscomments are stored in xattr user.xdg.commentdepends on python-pyxattrbecause files are so often over-written, save a copyof the comments in a database ~/.dirnotes.dbthese comments stick to the symlink"""import sys,os,argparsefrom dirWidget2 import DirWidgetfrom Tkinter import *from ttk import *import xattr, sqlite3, timeVERSION = "0.2"COMMENT_KEY = "user.xdg.comment"DATABASE_NAME = "~/.dirnotes.db"# convert the ~/ form to a fully qualified pathDATABASE_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 Falseclass 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 directoriesuse os.isfile()os.isdir()current, dirs, files = os.walk("path").next()possible set folllowLinks=True'''''' notes from the wdrm projecttable showed filename, size, date size, date, descat start, fills the list of all the filesskip the . entry'''''' should we also do user.xdg.tags="TagA,TagB" ?user.charsetuser.creator=application_name or user.xdg.creatoruser.xdg.origin.urluser.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'''
 |