| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 | #!/usr/bin/python3# TODO: get rid of the -n option; multiple files are prefixed by 'filename:', single files aren'tVERSION = "0.3"import os, sys, argparse, xattr, json, sqlite3answer_json = []verbose = 0db = Nonexattr_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)
 |