|
@@ -0,0 +1,205 @@
|
|
|
|
+#!/usr/bin/python3
|
|
|
|
+# TODO: get rid of the -n option; multiple files are prefixed by 'filename:', single files aren't
|
|
|
|
+
|
|
|
|
+VERSION = "0.3"
|
|
|
|
+
|
|
|
|
+import os, sys, argparse, xattr, json, sqlite3
|
|
|
|
+
|
|
|
|
+answer_json = []
|
|
|
|
+verbose = 0
|
|
|
|
+db = None
|
|
|
|
+xattr_comment = "user.xdg.comment"
|
|
|
|
+xattr_author = "user.xdg.comment.author"
|
|
|
|
+xattr_date = "user.xdg.comment.date"
|
|
|
|
+
|
|
|
|
+#======= debugging/verbose ===========
|
|
|
|
+def print_d(*a):
|
|
|
|
+ if verbose > 1:
|
|
|
|
+ print('>>',*a)
|
|
|
|
+def print_v(*a):
|
|
|
|
+ if verbose:
|
|
|
|
+ print('>',*a)
|
|
|
|
+
|
|
|
|
+#============= the functions that are called from the main.loop ===============
|
|
|
|
+
|
|
|
|
+def file_copy(f,target,target_is_dir,force):
|
|
|
|
+ print_d(f"call file_copy with args={target},{target_is_dir} and {force}")
|
|
|
|
+ dest = target if not target_is_dir else target+'/'+os.path.basename(f)
|
|
|
|
+ if os.path.exists(dest) and not force:
|
|
|
|
+ go = input("The copy target <<" + dest + ">> exists. Overwrite? (y or n) ")
|
|
|
|
+ if go != 'y' and go != 'Y':
|
|
|
|
+ return
|
|
|
|
+ print_d(f"copy from {f} to {dest}")
|
|
|
|
+
|
|
|
|
+def file_zap(f,all_flag):
|
|
|
|
+ print_d(f"zapping the comment history of {f}")
|
|
|
|
+ if all_flag:
|
|
|
|
+ print_d("zapping the entire database")
|
|
|
|
+
|
|
|
|
+def file_modify_comment(f, create, append, erase):
|
|
|
|
+ print_d(f"modify the comment on file {f} with extra={(create,append,erase)}")
|
|
|
|
+ if not os.path.exists(f):
|
|
|
|
+ print(f"the target file does not exist; please check the spelling of the file: {f}")
|
|
|
|
+ # sys.exit() here?
|
|
|
|
+
|
|
|
|
+def file_display(f, listall, history, json, minimal):
|
|
|
|
+ print_d(f"list file details {f}")
|
|
|
|
+ x_comment = None
|
|
|
|
+ try:
|
|
|
|
+ x_comment = xattr.getxattr(f,xattr_comment).decode()
|
|
|
|
+ x_author = xattr.getxattr(f,xattr_author).decode()
|
|
|
|
+ x_date = xattr.getxattr(f,xattr_date).decode()
|
|
|
|
+ except:
|
|
|
|
+ pass
|
|
|
|
+ full_f = os.path.realpath(f)
|
|
|
|
+
|
|
|
|
+ d_comment = getDbComment(full_f)
|
|
|
|
+ if d_comment:
|
|
|
|
+ d_comment, d_author, d_date = d_comment
|
|
|
|
+ print_d(f"for file {f}, database comment is <{d_comment}>, xattr comment is <{x_comment}>")
|
|
|
|
+
|
|
|
|
+ if os.path.isdir(f):
|
|
|
|
+ f = f+'/'
|
|
|
|
+ if x_comment or listall:
|
|
|
|
+ if x_comment and (d_comment != x_comment):
|
|
|
|
+ x_comment += '*'
|
|
|
|
+ if not json:
|
|
|
|
+ if minimal:
|
|
|
|
+ print(f"{x_comment}")
|
|
|
|
+ else:
|
|
|
|
+ print(f"{f}: {x_comment}")
|
|
|
|
+ else:
|
|
|
|
+ if verbose:
|
|
|
|
+ answer_json.append( {"file":f,"comment":x_comment,"author":x_author,"date":x_date } )
|
|
|
|
+ else:
|
|
|
|
+ answer_json.append( {"file":f,"comment":x_comment} )
|
|
|
|
+
|
|
|
|
+def getDbComment(full_filename):
|
|
|
|
+ global db
|
|
|
|
+ print_d(f"db access for {full_filename}")
|
|
|
|
+ c = db.cursor()
|
|
|
|
+ c.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(full_filename,))
|
|
|
|
+ a = c.fetchone()
|
|
|
|
+ if a:
|
|
|
|
+ return a[0:3]
|
|
|
|
+
|
|
|
|
+def openDb():
|
|
|
|
+ global db
|
|
|
|
+ dbName = "~/.dirnotes.db"
|
|
|
|
+ dbName = os.path.expanduser(dbName)
|
|
|
|
+ db = sqlite3.connect(dbName)
|
|
|
|
+ try:
|
|
|
|
+ c = db.cursor()
|
|
|
|
+ c.execute("select * from dirnotes")
|
|
|
|
+ except sqlite3.OperationalError:
|
|
|
|
+ c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
|
|
|
|
+ return db
|
|
|
|
+
|
|
|
|
+def main(args):
|
|
|
|
+ parser = argparse.ArgumentParser(description="Display or add comments to files",
|
|
|
|
+ epilog="Some options conflict. Use only one of: -l -c -a -H -e -z -Z and one of -d -x")
|
|
|
|
+ parser.add_argument('-V',"--version", action="version", version=f"dncli ver:{VERSION}")
|
|
|
|
+ parser.add_argument('-v',"--verbose", action='count', help="verbose, almost debugging; do not use in scripts",default=0)
|
|
|
|
+ parser.add_argument('-j',"--json", action="store_true",help="output in JSON format")
|
|
|
|
+ pars_m = parser.add_mutually_exclusive_group()
|
|
|
|
+ pars_m.add_argument('-l',"--listall", action="store_true",help="list all files, including those without comments")
|
|
|
|
+ parser.add_argument('-d',"--db", action="store_true",help="list comments from database")
|
|
|
|
+ parser.add_argument('-x',"--xattr", action="store_true",help="list comments from xattr")
|
|
|
|
+ parser.add_argument('-n',"--minimal", action="store_true",help="output only comments; useful in scripting")
|
|
|
|
+ parser.add_argument('-H',"--history", action="store_true",help="output the history of comments for a file")
|
|
|
|
+ pars_m.add_argument('-c',"--create", metavar="comment", help="add a comment to a file")
|
|
|
|
+ pars_m.add_argument('-a',"--append", metavar="comment", help="append to a comment on a file, separator=';'")
|
|
|
|
+ pars_m.add_argument('-C',"--copy", action="store_true",help="copy a file with its comments")
|
|
|
|
+ parser.add_argument('-y',"--cp_force",action="store_true",help="copy over existing files")
|
|
|
|
+ pars_m.add_argument('-e',"--erase", action="store_true",help="erase the comment on a file")
|
|
|
|
+ pars_m.add_argument('-z',"--zap", action="store_true",help="clear the comment history on a file")
|
|
|
|
+ pars_m.add_argument('-Z',"--zapall", action="store_true",help="clear the comment history in the entire database")
|
|
|
|
+ parser.add_argument('file_list',nargs='*',help="file(s); list commands may omit this")
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
+
|
|
|
|
+ # default is to display all files that have comments
|
|
|
|
+
|
|
|
|
+ # major modes are: display (<none> -l -H), add-comment (-a -c -e), clear-history(-z -Z), copy (-C)
|
|
|
|
+ # determine the major mode, then apply an appropriate function over the file_list
|
|
|
|
+
|
|
|
|
+ args.display = not (args.create or args.append or args.copy or args.erase or args.zap or args.zapall)
|
|
|
|
+ if args.cp_force and not args.copy:
|
|
|
|
+ print("the -y/--cp_force options can only be used with the -C/--copy command")
|
|
|
|
+ sys.exit(3)
|
|
|
|
+ if args.json and not args.display:
|
|
|
|
+ print("the -j/--json option can only be used with the display modes")
|
|
|
|
+ sys.exit(4)
|
|
|
|
+ if args.minimal and not args.display:
|
|
|
|
+ print("the -n/--minimal option only applies to the display modes")
|
|
|
|
+ sys.exit(5)
|
|
|
|
+ if args.history and not args.display:
|
|
|
|
+ print("the -H/--history option only applies to the display modes")
|
|
|
|
+ sys.exit(5)
|
|
|
|
+ if args.xattr and (args.zap or args.zapall):
|
|
|
|
+ print("the -x/--xattr option doesn't apply to the -z/--zap and -Z/--zapall commands")
|
|
|
|
+ sys.exit(7)
|
|
|
|
+
|
|
|
|
+ global verbose
|
|
|
|
+ verbose = args.verbose
|
|
|
|
+
|
|
|
|
+ #====== 1) build the file list =============
|
|
|
|
+
|
|
|
|
+ files = args.file_list
|
|
|
|
+ # for the list commands, auto-fill the file list with the current directory
|
|
|
|
+ if not files and args.display:
|
|
|
|
+ files = os.listdir(".")
|
|
|
|
+ files.sort()
|
|
|
|
+ # other command require explicity file lists, but 'dncli -c "new comment" *' will work
|
|
|
|
+ if not files:
|
|
|
|
+ print("please specify a file or files to use")
|
|
|
|
+ sys.exit(10)
|
|
|
|
+ print_d("got the files:", files)
|
|
|
|
+
|
|
|
|
+ #======= 2) build the function
|
|
|
|
+ if args.create or args.append or args.erase:
|
|
|
|
+ print_d(f"create/append/erase: {args.create} . {args.append} . {args.erase}")
|
|
|
|
+ func = file_modify_comment
|
|
|
|
+ loop_args = (args.create, args.append, args.erase)
|
|
|
|
+ elif args.zap or args.zapall:
|
|
|
|
+ print_d(f"got a zap command {args.zap} . {args.zapall}")
|
|
|
|
+ func = file_zap
|
|
|
|
+ loop_args = (args.zapall,)
|
|
|
|
+ if args.zapall:
|
|
|
|
+ files = (1,)
|
|
|
|
+ elif args.copy:
|
|
|
|
+ print_d(f"got a copy command to {args.copy}")
|
|
|
|
+ # the last item on the file list is the target
|
|
|
|
+ n_files = len(files)
|
|
|
|
+ if n_files < 2:
|
|
|
|
+ print_d(f"the copy command requires at least two arguments, the last one is the destination")
|
|
|
|
+ sys.exit(1)
|
|
|
|
+ target = files[-1]
|
|
|
|
+ target_is_directory = os.path.isdir(target)
|
|
|
|
+ files = files[:-1]
|
|
|
|
+ print_d(f"copy from {files} to {target}")
|
|
|
|
+ if n_files > 2 and not target_is_directory:
|
|
|
|
+ print_d(f"multiple copy files must go to a target directory")
|
|
|
|
+ sys.exit(3)
|
|
|
|
+ func = file_copy
|
|
|
|
+ loop_args = (target, target_is_directory, args.cp_force)
|
|
|
|
+ else:
|
|
|
|
+ assert(args.display)
|
|
|
|
+ print_v(f"list files using {'database' if args.db else 'xattr'} priority")
|
|
|
|
+ print_d(f"display command with option: {args.listall} and {args.history} and {args.json} and {args.minimal}")
|
|
|
|
+ loop_args = (args.listall, args.history, args.json, args.minimal)
|
|
|
|
+ func = file_display
|
|
|
|
+
|
|
|
|
+ #====== 3) loop on the list, execute the function =============
|
|
|
|
+ global db
|
|
|
|
+ db = openDb()
|
|
|
|
+
|
|
|
|
+ for f in files:
|
|
|
|
+ func(f,*loop_args)
|
|
|
|
+
|
|
|
|
+ if answer_json:
|
|
|
|
+ print(json.dumps(answer_json))
|
|
|
|
+
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
+ main(sys.argv)
|
|
|
|
+
|
|
|
|
+
|