dirnotes2 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. #!/usr/bin/python
  2. """ a simple gui or command line app
  3. to view and create/edit file comments
  4. comments are stored in xattr user.xdg.comment
  5. depends on python-pyxattr
  6. because files are so often over-written, save a copy
  7. of the comments in a database ~/.dirnotes.db
  8. these comments stick to the symlink
  9. """
  10. import sys,os,argparse
  11. from dirWidget2 import DirWidget
  12. from Tkinter import *
  13. from ttk import *
  14. import xattr, sqlite3, time
  15. VERSION = "0.2"
  16. COMMENT_KEY = "user.xdg.comment"
  17. DATABASE_NAME = "~/.dirnotes.db"
  18. # convert the ~/ form to a fully qualified path
  19. DATABASE_NAME = os.path.expanduser(DATABASE_NAME)
  20. class DataBase:
  21. ''' the database is flat
  22. fileName: fully qualified name
  23. st_mtime: a float
  24. size: a long
  25. comment: a string
  26. comment_time: a float, the time of the comment save
  27. this is effectively a log file, as well as a resource for a restore
  28. (in case a file-move is done without comment)
  29. the database is associated with a user, in the $HOME dir
  30. '''
  31. def __init__(self):
  32. '''try to open the database; if not found, create it'''
  33. try:
  34. self.db = sqlite3.connect(DATABASE_NAME)
  35. except sqlite3.OperationalError:
  36. print("Database %s not found" % DATABASE_NAME)
  37. raise OperationalError
  38. c = self.db.cursor()
  39. try:
  40. c.execute("select * from dirnotes")
  41. except sqlite3.OperationalError:
  42. print("Table %s created" % ("dirnotes"))
  43. c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME)")
  44. def getData(self, fileName):
  45. c = self.db.cursor()
  46. c.execute("select * from dirnotes where name=? order by comment_date desc",(os.path.abspath(fileName),))
  47. return c.fetchone()
  48. def setData(self, fileName, _date, _size, comment):
  49. c = self.db.cursor()
  50. c.execute("insert into dirnotes values (?,?,?,?,?)",
  51. (fileName, _date, _size, comment, time.time()))
  52. self.db.commit()
  53. return True
  54. def log(self, fileName, comment):
  55. ''' TODO: convert filename to canonical '''
  56. c = self.db.cursor()
  57. s = os.stat(fileName)
  58. print ("params: %s %f %d %s %f" % ((os.path.abspath(fileName), s.st_mtime, s.st_size, comment, time.time())))
  59. c.execute("insert into dirnotes values (?,datetime(?,'unixepoch','localtime'),?,?,datetime(?,'unixepoch','localtime'))",
  60. (os.path.abspath(fileName), s.st_mtime, s.st_size, str(comment), time.time()))
  61. self.db.commit()
  62. def parse():
  63. parser = argparse.ArgumentParser(description='dirnotes application')
  64. parser.add_argument('dirname',metavar='dirname',type=str,
  65. help='directory [default=current dir]',default=".",nargs='?')
  66. parser.add_argument('dirname2',help='comparison directory, shows two-dirs side-by-side',nargs='?')
  67. parser.add_argument('-n','--nogui',action="store_const",const="1",
  68. help='use text base interface')
  69. parser.add_argument('-v','--version',action='version',version='%(prog)s '+VERSION)
  70. group = parser.add_mutually_exclusive_group()
  71. group.add_argument('-s','--sort-by-name',metavar='sort',action="store_const",const='n')
  72. group.add_argument('-m','--sort-by-date',metavar='sort',action='store_const',const='d')
  73. return parser.parse_args()
  74. class FileObj():
  75. def __init__(self, fileName):
  76. self.fileName = fileName
  77. # also get the date, directory or not, etc
  78. self.comment = ''
  79. try:
  80. self.comment = xattr.getxattr(fileName,COMMENT_KEY)
  81. except Exception as e:
  82. #print("comment read on %s failed, execption %s" % (self.fileName,e))
  83. pass
  84. def getName(self):
  85. return self.fileName
  86. def getShortName(self):
  87. if self.fileName[-1]=='/': #do show dirs, they can have comments
  88. return os.path.basename(self.fileName[:-1])+'/'
  89. else:
  90. return os.path.basename(self.fileName)
  91. def getComment(self):
  92. return self.comment
  93. def setComment(self,newComment):
  94. self.comment = newComment
  95. try:
  96. xattr.setxattr(self.fileName,COMMENT_KEY,self.comment)
  97. return True
  98. # we need to move these cases out to a handler
  99. except Exception as e:
  100. print("problem setting the comment on file %s" % (self.fileName,))
  101. if os.access(self.fileName, os.W_OK)!=True:
  102. print("you don't appear to have write permissions on this file")
  103. # change the listbox background to yellow
  104. self.displayBox.notifyUnchanged()
  105. elif "Errno 95" in str(e):
  106. print("is this a VFAT or EXFAT volume? these don't allow comments")
  107. return False
  108. class DirNotes(Frame):
  109. ''' the main window of the app
  110. has 3 list boxes: dir_left, dir_right (may be invisible) and files
  111. '''
  112. def __init__(self, parent, filename, db):
  113. Frame.__init__(self,parent)
  114. self.db = db
  115. self.lb = lb = Treeview(self)
  116. lb['columns'] = ('comment')
  117. lb.heading('#0',text='Name')
  118. lb.heading('comment',text='Comment')
  119. # resize the comments column
  120. # and resize the parent window to match the directory size
  121. # allow multiple entries on the line at this point
  122. #d = os.listdir(p.filename[0])
  123. #d.sort()
  124. current, dirs, files = os.walk(filename,followlinks=True).next()
  125. dirs = map(lambda x:x+'/', dirs)
  126. dirs.sort()
  127. files.sort()
  128. d = dirs + files
  129. self.files = []
  130. for i in range(len(d)):
  131. this_file = FileObj(current+'/'+d[i])
  132. self.files = self.files + [this_file]
  133. lb.insert('','end',iid=str(i),text=this_file.getShortName(),)
  134. #lb.itemAt(i,0).setFlags(Qt.ItemIsEnabled) #NoItemFlags)
  135. comment = this_file.getComment()
  136. lb.set(item=str(i),column='comment',value=comment)
  137. e2 = Label(self,text="View and edit file comments stored in extended attributes user.xdg.comment")
  138. e1 = Label(self,text="Active Directory:")
  139. b1 = Button(self,text="restore from database")
  140. dirLeft = DirWidget(self,current)
  141. #dirLeft.setMaximumHeight(140)
  142. #dirLeft.setMaximumWidth(200)
  143. dirRight = DirWidget(self,current)
  144. #~ dirRight.setMaximumHeight(140)
  145. #~ dirRight.setMaximumWidth(200)
  146. #~ dirRight.setEnabled(False)
  147. #~ layout = QVBoxLayout()
  148. #~ upperLayout = QHBoxLayout()
  149. #~ layout.addWidget(e)
  150. #~ upperLayout.addWidget(dirLeft)
  151. #~ upperLayout.addWidget(b1)
  152. #~ upperLayout.addWidget(dirRight)
  153. #~ layout.addLayout(upperLayout)
  154. #~ layout.addWidget(lb)
  155. #~ win.setLayout(layout)
  156. #~ lb.itemChanged.connect(self.change)
  157. #~ b1.pressed.connect(self.restore_from_database)
  158. #~ QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
  159. #~ self.setWindowTitle("test")
  160. #~ self.setMinimumSize(600,400)
  161. e1.pack(anchor=W,padx=20)
  162. dirLeft.pack(anchor=W,padx=20,pady=5)
  163. e2.pack()
  164. lb.pack()
  165. def closeEvent(self,e):
  166. print("closing")
  167. def change(self,x):
  168. print("debugging " + x.text() + " r:" + str(x.row()) + " c:" + str(x.column()))
  169. the_file = dn.files[x.row()]
  170. r = the_file.setComment(str(x.text()))
  171. if r:
  172. self.db.log(the_file.getName(),x.text())
  173. def restore_from_database(self):
  174. print("restore from database")
  175. fileName = str(self.lb.item(self.lb.currentRow(),0).text())
  176. fo_row = self.db.getData(fileName)
  177. if len(fo_row)>1:
  178. comment = fo_row[3]
  179. print(fileName,fo_row[0],comment)
  180. if __name__=="__main__":
  181. p = parse()
  182. if p.dirname[-1]=='/':
  183. p.dirname = p.dirname[:-1]
  184. print(p.dirname)
  185. db = DataBase()
  186. tk_basis = Tk()
  187. tk_basis.title("DirNotes "+p.dirname)
  188. dn = DirNotes(tk_basis,p.dirname,db)
  189. dn.pack()
  190. mainloop()
  191. #xattr.setxattr(filename,COMMENT_KEY,commentText)
  192. ''' files from directories
  193. use os.isfile()
  194. os.isdir()
  195. current, dirs, files = os.walk("path").next()
  196. possible set folllowLinks=True'''
  197. ''' notes from the wdrm project
  198. table showed
  199. filename, size, date size, date, desc
  200. at start, fills the list of all the files
  201. skip the . entry
  202. '''
  203. ''' should we also do user.xdg.tags="TagA,TagB" ?
  204. user.charset
  205. user.creator=application_name or user.xdg.creator
  206. user.xdg.origin.url
  207. user.xdg.language=[RFC3066/ISO639]
  208. user.xdg.publisher
  209. '''
  210. ''' to allow column-sorting, you use the sortByColumn and set the Horiz-header to clickable
  211. '''
  212. ''' TODO: also need a way to display-&-restore comments from the database '''
  213. ''' QFileDialog
  214. -make my own?
  215. -existing one has
  216. -history
  217. -back button
  218. -up button
  219. -but we don't need
  220. -directory date
  221. -icon option
  222. -url browser (unless we go network file system)
  223. -new folder button
  224. -file type chooser
  225. -text entry box
  226. -choose & cancel buttons
  227. '''
  228. '''
  229. http://stackoverflow.com/questions/18562123/how-to-make-ttk-treeviews-rows-editable
  230. '''