#!/usr/bin/python3 # TODO starting with a dir shoild list all-files # 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 ( -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)