diff --git a/README.md b/README.md index 04d8f58..e5931d3 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,39 @@ # image-index This project is a script which can index and sort files on your pc. You can give every file a title, category, source, tags and content for easier finding later on. All information is stored inside of an SQLite-database. Two separate tables are used to give aliases to the categories and tags, so that a user can easily change the names without modifying all affected entries. +You can also encrypt all files with AES-CBC. + +This project was written and tested on an Arch Linux-based distribution and python 3.10. ## Functions * add - Add a file and entry to the index +* copy - copies a file from the index to a custom location * check - check if all files saved in the index exist and aren't faulty * delete - delete a file and remove the entry -* meta - a command to change aliases of categorties and tags +* import - lets you add every file to the index in a chosen directory +* meta - a command to change aliases of categories and tags * open - open one or more files from the index in the default application (only Linux and Windows) +* replace - replaces a file in the index with another file while keeping the entry * show - search through the index and show the matches +* stats - shows statistics about the index like number of entries, tags and categories * update - change a value of an entry in the index or move a file to another category -## Installation +## Dependencies +* hashlib +* pycryptodomex +* sqlite3 + +## Downloading ```sh -git clone https://gitlab.com/rodin_schule/image-index-py.git +git clone https://git.marcelsite.com/marcel/image-index-py.git cd ./image-index-py -chmod u+x image-index ``` ## Configuration The top of the file holds some very important variables that need to be looked at by the user: * ROOT_DIR: The absolute path of where you want to save your files (the directories for the categories will be created there) -* CONFIG_DIR: The absolute path of where you want to store your `index.db`-file. -* LINUX_APP_STARTER: The linux command which can open a file in the default application. Most distributions use `xdg-open`. \ No newline at end of file +* INDEX_FILE: The absolute path of the database file. +* LINUX_APP_STARTER: The linux command which can open a file in the default application. Most distributions use `xdg-open`. +* ENCRYPT: This setting tells the script whether to encrypt the added files by default or not. + * If set to True, the ids of the categories will be randomly generated (fe627ea4-3fd60 instead of category-3fd60) for pretty much zero-knowledge storage on a remote server without access to the database. +* MAX_ITEMS: The maximal amount of entries shown in the selection menu. \ No newline at end of file diff --git a/image-index b/image-index index 49db85d..1d163ae 100755 --- a/image-index +++ b/image-index @@ -1,15 +1,18 @@ -#!/bin/python3 -import hashlib,os,random,re,shutil,subprocess,sys -from pathlib import Path +#!/usr/bin/python3 +import ast,hashlib,os,random,re,secrets,shutil,subprocess,sys,tempfile from uuid import uuid4 import sqlite3 as sql +from base64 import b64encode,b64decode +from Cryptodome.Cipher import AES ROOT_DIR=os.getcwd() # The directory where all directories and files of the index are located -CONFIG_DIR=ROOT_DIR # The directory where the file 'index.db' is located -LINUX_APP_STARTER="xdg-open" # The command which opens the files in the default applications +INDEX_FILE=ROOT_DIR+"/index.db" # The database file +LINUX_APP_STARTER="xdg-open" # The command which opens the files in the default application +ENCRYPT=True # True or False; Whether the default is to encrypt the file or to save it as a plain file +MAX_ITEMS=10 # Maximal amount of shown entries for selection class database(): - def __init__(self,filepath = CONFIG_DIR + "/index.db"): + def __init__(self,filepath = INDEX_FILE): self.connection=None self.crsr=None if not os.path.exists(filepath) : @@ -18,10 +21,11 @@ class database(): self.connection = sql.connect(filepath) self.crsr = self.connection.cursor() - def add_index(self,vallist): + def add_index(self,vallist,collist=""): + collist=self.collist if not collist else collist # compile the options into a command for the SQLite database - colstring=",".join(self.collist) - valstring="'{}'".format("','".join(vallist)) + colstring=",".join(collist) + valstring='"{}"'.format('","'.join(vallist)) self.crsr.execute("""INSERT INTO {table} ({cols}) VALUES ({vals}); """.format(table=self.name,cols=colstring,vals=valstring)) @@ -43,21 +47,25 @@ class database(): self.crsr.execute(sqlcommand) sqlcommand = """CREATE TABLE CATEGORY( NAME TEXT PRIMARY KEY NOT NULL, - ALIAS TEXT, - DESCRIPTION TEXT + ALIAS TEXT ); """ self.crsr.execute(sqlcommand) sqlcommand = """CREATE TABLE TAGS( NAME TEXT PRIMARY KEY NOT NULL, - ALIAS TEXT, - DESCRIPTION TEXT + ALIAS TEXT + ); """ + self.crsr.execute(sqlcommand) + sqlcommand = """CREATE TABLE ENCRYPTION( + NAME TEXT PRIMARY KEY NOT NULL, + CIPHER TEXT NOT NULL, + PASSWORD TEXT ); """ self.crsr.execute(sqlcommand) def delete_index(self,typ,item): self.crsr.execute("DELETE FROM {} WHERE {}='{}'".format(self.name,typ,item)) self.connection.commit() - return + return True def get_col(self,column = "*"): # get the column of some table. If no options given, return all columns @@ -66,9 +74,9 @@ class database(): res=[] for i in tres: res.append(i) - # if the table is empty, return ".". + # if the table is empty, return "". if not res: - res="." + res="" return res def get_item(self,column,where,specific=False): @@ -94,17 +102,11 @@ class database(): n=0 res=[] for i in tres: - m=0 - for j in tres: - if i in j and n!=m: - continue - else: - res.append(i) - m+=1 - n+=1 - # if the table is empty, return ".". + if not i in res: + res.append(i) + # if the table is empty, return "". if not res: - return ["."] + return [""] return res def select_index(self,sel_list,quiet=False): @@ -115,7 +117,7 @@ class database(): if len(sel_list) > 1: n=0 print("Found several matches:") - for tup in sel_list: + for tup in sel_list[:MAX_ITEMS]: temp_list=[] for j in tup: temp_list.append(j) @@ -123,7 +125,7 @@ class database(): if self.name == "FILES": print("\tTitle:\t ",temp_list[2]) name=ctb.get_alias(temp_list[4]) - if name != ".": + if name != "": category=name else: category=temp_list[4] @@ -131,7 +133,7 @@ class database(): tags_list=[] for tag in temp_list[5].split(","): name=ttb.get_alias(tag) - if name != ".": + if name != "": tag=name tags_list.append(tag) print("\tTags:\t ",",".join(tags_list)) @@ -140,9 +142,11 @@ class database(): print("\tAlias:\t ",temp_list[1]) print("\tDescription: ",temp_list[2]) n+=1 + if len(sel_list) > MAX_ITEMS: + print(f"The list was too long, so it was reduced to {MAX_ITEMS} entries.") eingabe=input("Enter number(s) (0-{}; '*' for all entries): ".format(n-1)) if not eingabe: - return ["."] + return [""] num_list=[] if re.match('[*]',eingabe): for i in range(0,n): @@ -159,10 +163,10 @@ class database(): if not quiet: print("\nFinal match{}:".format("es" if len(num_list)-nminus > 1 else "")) else: - if sel_list[0] == ".": + if sel_list[0] == "": if not quiet == "strict": print("No matching entry found!") - return ["."] + return [""] res=sel_list if not quiet == "strict": print("\nMatch found!") @@ -178,10 +182,12 @@ class database(): if not secondlist: n=0 for i in self.get_col(typ): + if len(temp_list) > MAX_ITEMS: + break aliases=[] - if i == "." and self.name == "FILES": + if i == "" and self.name == "FILES": print("NO ENTRIES IN THE INDEX!") - return "." + return "" # get aliases for the checks, but only for unspecific search! if self.name == "FILES": if typ == "*": @@ -222,7 +228,7 @@ class database(): n+=1 else: - if not secondlist[0] == ".": + if not secondlist[0] == "": for i in secondlist: for j in firstlist: if j in i: @@ -231,8 +237,8 @@ class database(): else: return secondlist if not temp_list: - return ["."] - return temp_list + return [""] + return temp_list[:MAX_ITEMS+1] return secondlist def update_index(self,typ,update,where,val): @@ -240,50 +246,167 @@ class database(): self.connection.commit() return True -class metatable(database): - def __init__(self,typ,filepath = CONFIG_DIR + "/index.db"): - self.name=typ.upper() - self.collist=["NAME","ALIAS","DESCRIPTION"] +class enctable(database): # https://www.thesecuritybuddy.com/cryptography-and-python/aes-encryption-and-decryption-using-pycryptodome-module-in-python/ + def __init__(self, filepath = INDEX_FILE): + self.name="ENCRYPTION" + self.collist=["NAME","CIPHER","PASSWORD"] super().__init__(filepath) - def add_index(self,val,alias,randhex=""): - super().add_index([val.lower() + "-" + randhex,alias,'']) + def derive_key_and_iv(self, password, salt, key_length, iv_length): # derive key and IV from password and salt. + d = d_i = b'' + while len(d) < key_length + iv_length: + d_i = hashlib.md5(d_i + password + salt).digest() # obtain the md5 hash value + d += d_i + return d[:key_length], d[key_length:key_length+iv_length] + + def encrypt(self, in_file, out_filepath, password="", key_length=32): + print("Encrypting...") + in_file=open(in_file,"rb") + out_uuid=out_filepath.split("/")[-1].split(".")[0] + out_file=open(f"{out_filepath}","wb") + bs = AES.block_size # 16 bytes + if not password: + password = os.urandom(bs*random.randint(1,4)) + if self.get_item("NAME", out_uuid)[0] == "": + self.add_index([out_uuid.split(".")[0],"AES",b64encode(password).decode()]) + else: + password_list=self.get_item("NAME", in_uuid.split(".")[0]) + if password_list[0] != "": + if len(password_list) == 1: + password=b64decode(password_list[0][2].encode()) + else: + print("ERROR: MULTIPLE PASSWORD ENTRIES FOUND!") + return False + salt = os.urandom(bs) # return a string of random bytes + key, iv = self.derive_key_and_iv(password, salt, key_length, bs) + cipher = AES.new(key, AES.MODE_CBC, iv) + out_file.write(salt) + finished = False + + while not finished: + chunk = in_file.read(1024 * bs) + if len(chunk) == 0 or len(chunk) % bs != 0:# final block/chunk is padded before encryption + padding_length = (bs - len(chunk) % bs) or bs + chunk += str.encode(padding_length * chr(padding_length)) + finished = True + out_file.write(cipher.encrypt(chunk)) + out_file.close() + in_file.close() + + def is_encrypted(self, uuid): + if self.get_item("NAME", uuid)[0] != "": + return True + return False + + def decrypt(self, in_filepath, out_file=None, password="", key_length=32): + print("Decrypting...") + in_file=open(f"{in_filepath}","rb") # open the encrypted file + in_uuid=in_filepath.split("/")[-1] + if not out_file: + out_temp=tempfile.mkstemp(prefix="image-index-") + filepath=out_temp[1] + out_file=open(filepath,"wb") + else: + filepath=out_file + out_file=open(out_file,"wb") + if not password: + password_list=self.get_item("NAME", in_uuid.split(".")[0]) + if password_list[0] != "": + if len(password_list) == 1: + password=b64decode(password_list[0][2].encode()) + elif len(password_list) > 1: + print("ERROR: MULTIPLE PASSWORD ENTRIES FOUND!") + return False + else: + print("ERROR: NO PASSWORD FOUND FOR DECRYPTION!") + return False + else: + password=b64decode(password.encode()) + bs = AES.block_size + salt = in_file.read(bs) + key, iv = self.derive_key_and_iv(password, salt, key_length, bs) + cipher = AES.new(key, AES.MODE_CBC, iv) + next_chunk = '' + finished = False + while not finished: + chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs)) + if len(next_chunk) == 0: + padding_length = chunk[-1] + chunk = chunk[:-padding_length] + finished = True + out_file.write(bytes(x for x in chunk)) + out_file.close() + in_file.close() + return filepath + + def delete_index(self, uuid): + super().delete_index("NAME", uuid) + print("UUID",uuid) + return True + +class metatable(database): + def __init__(self,typ,filepath = INDEX_FILE): + self.name=typ.upper() + self.collist=["NAME","ALIAS"] + super().__init__(filepath) + + def add_index(self,val,alias,randhex=None): + if not randhex: + randhex=get_randchar() + if self.name == "TAGS" or bencrypt: + val=get_randchar(8) + else: + val=re.sub('[ ,?!/\\:!*"<>|]', '', val) + super().add_index([val[:8] + "-" + randhex,alias]) def check_index(self,typ): res=[] for i in tb.get_col(typ): success=0 for j in self.get_col("NAME"): - if j[0] in i[0].lower(): + if j[0].split(".")[0] in i[0]: success=1 if not i[0] in res and success == 0: res.append(i[0]) return res def get_alias(self,arg): + if not arg: + return "" selection=self.search_index(arg,"strict") item=selection[0] - if item[0] != ".": + if item[0] != "": alias=item[1] else: - alias = "." + alias = "" return alias def get_name(self,arg): + if not arg: + return "" selection=self.search_index(arg,"strict") item=selection[0] - name=item[0] + if not item: + name="" + else: + name=item[0] return name def search_index(self,args,quiet=True): selection=[] selection=self.sql_compare_list("*", [args], selection,True) + if selection[0] == "": + slist=args.split(" ") + selection=self.sql_compare_list("*", slist, [],False) + if len(selection) > 1: + print("Please enter a more specific search query!") + return "" selection=self.select_index(selection,quiet) return selection def update_index(self, typ, update, where, val): selection=self.search_index(update,"strict") - if selection[0] != "." and len(selection) >= 1: + if selection[0] != "" and len(selection) >= 1: print("One entry is already called {}!".format(update)) return False else: @@ -291,7 +414,7 @@ class metatable(database): return True class filestable(database): - def __init__(self,filepath = CONFIG_DIR + "/index.db"): + def __init__(self,filepath = INDEX_FILE): self.name="FILES" self.collist=["FILE","HASH","TITLE","SOURCE","CATEGORY","TAGS","CONTENT"] super().__init__(filepath) @@ -299,88 +422,128 @@ class filestable(database): def add_index(self,filepath,category="default",title="",source="",tags="",content=""): filehash=self.get_hash(filepath) # make hash of file before copy if filehash in str(self.get_col("HASH")): - print("This file already exists!") - return + print("This file already has an entry!") + return False n=0 - randhex=get_randhex() # get the name of the category from the meta table if not category: category="default" name=ctb.get_name(category) - if name != ".": + if name: category=name else: - ctb.add_index(category.lower(),category,randhex) + ctb.add_index(category.lower(),category) category=ctb.get_name(category) # get the name of the tags from the meta table tags_list=[] for tag in tags.split(','): - randhex=get_randhex() name=ttb.get_name(tag) - if name != ".": + if name != "": tag=name else: - ttb.add_index(tag.lower(),tag,randhex) + ttb.add_index(tag.lower(),tag) tags_list.append(ttb.get_name(tag)) tags=",".join(tags_list) - filetype=os.path.splitext(filepath)[1] - filename=str(uuid4()) + filetype + fileext=os.path.splitext(filepath)[-1] + fileuuid=str(uuid4()) + filename=fileuuid+fileext if not os.path.exists("{}/{}".format(ROOT_DIR,category)): os.makedirs("{}/{}".format(ROOT_DIR,category)) # try to copy the file, return if error. try: - shutil.copy(filepath,"{}/{}/{}".format(ROOT_DIR,category,filename)) + if bencrypt: + etb.encrypt(filepath, f"{ROOT_DIR}/{category}/{fileuuid}.enc") + else: + shutil.copy(filepath,f"{ROOT_DIR}/{category}/{filename}") except Exception as e: print(e) print("COULDN'T COPY FILE TO DESTINATION!") - return + return False vallist=[filename,filehash,title,source,category,tags,content] super().add_index(vallist) + return True def check_index(self): hash_list=[] path_list=[] + enc_num=0 for i in self.get_col(): filename=i[0] + fileuuid=filename.split(".")[0] category=i[4] filehash1=i[1] + if etb.is_encrypted(fileuuid): + enc_num+=1 + filename=f"{fileuuid}.enc" filepath="{}/{}/{}".format(ROOT_DIR,category,filename) if not os.path.exists(filepath): path_list.append(i) + if etb.is_encrypted(fileuuid): + continue try: filehash2=self.get_hash(filepath) if not filehash1 == filehash2: hash_list.append(i) except Exception: path_list.append(i) - + print(f"Encrypted files: {enc_num}; hashes not checked.") return hash_list,path_list def delete_index(self,sel_list): - self.get_item("HASH",sel_list[1]) - super().delete_index("HASH",sel_list[1]) - - def update_index(self,typ,update,sel_list): + item_list=self.get_item(self.collist[1],sel_list[1]) + if len(item_list) == 1: + item=item_list[0] + category=item[4] + filename=item[0] + fileuuid=os.path.splitext(filename)[0] + super().delete_index(self.collist[1],sel_list[1]) + etb.delete_index(fileuuid) + return True + + def replace_file(self,in_filepath,item): + filehash=self.get_hash(in_filepath) + category=item[4] + filename=item[0] + if filehash in str(self.get_col("HASH")): + print("This file already has an entry!") + return False + fileuuid=os.path.splitext(filename)[-1] + out_filepath=f"{ROOT_DIR}/{category}/{filename}" + out_encpath=f"{ROOT_DIR}/{category}/{fileuuid}.enc" + try: + if etb.is_encrypted(fileuuid): + etb.encrypt(in_filepath, out_encpath) + else: + shutil.copy(in_filepath, out_filepath) + except Exception as e: + print("ERROR:",e) + return False + super().update_index("HASH", filehash, self.collist[0], filename) + return True + + def update_index(self,typ,update,sel_list,omnipotent=False): typ=typ.upper() - if typ in ["FILE","HASH"]: + if typ in ["FILE","HASH"] and not omnipotent: print("This type can't be changed!") - return 1 + return False category=sel_list[4] filehash=sel_list[1] filename=sel_list[0] - randhex=get_randhex() + fileuuid=sel_list[0].split(".")[0] if typ in ["CATEGORY"]: # get alias of category name=ctb.get_name(update) - if name != ".": + if name != "": update=name else: - ctb.add_index(update.lower(),update,randhex) + ctb.add_index(update.lower(),update) update=ctb.get_name(update) if not os.path.exists("{}/{}".format(ROOT_DIR,update)): os.makedirs("{}/{}".format(ROOT_DIR,update)) + if etb.is_encrypted(fileuuid): + filename=f"{fileuuid}.enc" shutil.move("{}/{}/{}".format(ROOT_DIR,category,filename), "{}/{}/{}".format(ROOT_DIR,update,filename)) if typ in ["TAGS"]: tags_list=[] @@ -388,10 +551,9 @@ class filestable(database): for tag in sel_list[5].split(","): tags_list.append(tag) for tag in update[1:].split(","): - randhex=get_randhex() name=ttb.get_name(tag) - if name == ".": - ttb.add_index(tag.lower(), tag, randhex) + if name == "": + ttb.add_index(tag.lower(), tag) name=ttb.get_name(tag) tags_list.append(name) elif update[0][0] == "-": @@ -399,8 +561,7 @@ class filestable(database): success=0 for i in update[1:].split(","): name=ttb.get_name(i) - if name == ".": - randhex=get_randhex() + if name == "": ttb.add_index(tag.lower(), tag) name=ttb.get_name(tag) if name == tag: @@ -409,12 +570,11 @@ class filestable(database): tags_list.append(tag) else: for tag in update.split(","): - randhex=get_randhex() name=ttb.get_name(tag) - if name != ".": + if name != "": tags_list.append(name) else: - ttb.add_index(tag.lower(), tag, randhex) + ttb.add_index(tag.lower(), tag) tags_list.append(ttb.get_name(tag)) update=",".join(tags_list) super().update_index(typ, update, "HASH", filehash) @@ -467,7 +627,7 @@ class filestable(database): alle.append(arg) elif snext == "category": name=ctb.get_name(arg) - if name != ".": + if name != "": arg=name category.append(arg) elif snext == "file": @@ -482,13 +642,13 @@ class filestable(database): '''if "," in arg: for tag in arg.split(","): name=ttb.get_name(arg) - if name != ".": + if name != "": arg=name tags.append(arg) else: name=ttb.get_name(arg) print("name",name) - if name != ".": + if name != "": arg=name''' tags.append(arg) else: @@ -506,10 +666,10 @@ class filestable(database): return self.select_index(selection,quiet) -def get_randhex(count=5): +def get_randchar(count=5): randhex="" for i in range(count): - randhex+=random.choice("0123456789abcdef") + randhex+=random.choice("0123456789abcdefghijklmnopqrstuvwxyz") return randhex def add(args): @@ -518,28 +678,64 @@ def add(args): return n=0 for i in ["Filepath","Category","Title","Source","Tags","Content"]: + i=i.title() try: print("{}: {}".format(i,args[n])) except Exception: + extra="" if i == "Tags": extra=" (Separate with ',')" - else: - extra="" - eingabe = input("Enter {}{}: ".format(i,extra)) + eingabe = input("{}{}: ".format(i,extra)) if i in ["Category"] and not eingabe: print("{} set to 'default'".format(i)) eingabe="default" args.append(eingabe) if i in ["Filepath"]: + filehash=tb.get_hash(args[n]) + if filehash in str(tb.get_col("HASH")): + print("This file already exists!") + return False if not args[n]: print("{} must not be empty!".format(i)) - return 1 + return False else: - if not Path(args[n]).is_file(): - print(" The file '{}' doesn't exist or is not a file!".format(eingabe)) - return 1 + if not os.path.isfile(args[n]): + print(" The file '{}' doesn't exist or is not a file!".format(args[n])) + return False n+=1 - tb.add_index(args[0],args[1],args[2],args[3],args[4],args[5]) + success=tb.add_index(args[0],args[1],args[2],args[3],args[4],args[5]) + if success: + print("Added '{}'!".format(args[2])) + +def copy(args): + if len(args) == 0: + out_filepath=input() + sel_list=search([]) + elif len(args) == 1: + out_filepath=args[0] + sel_list=search([]) + else: + out_filepath=args[0] + sel_list=search(args[1:]) + for item in sel_list: + filename=item[0] + out_fileext=os.path.splitext(filename)[-1] + title=item[2] + category=item[4] + fileuuid=os.path.splitext(filename)[0] + extra="" + if os.path.exists(out_filepath): + if os.path.isfile(out_filepath): + if re.match('[nN].*', input(f"The file on path {out_filepath} already exists!\nDo you want to overwrite it? [Y/n] ")): + return False + if os.path.isdir(out_filepath): + print("found DIRECTORY") + extra=f"/{title}{out_fileext}" + if etb.is_encrypted(fileuuid): + etb.decrypt(f"{ROOT_DIR}/{category}/{fileuuid}.enc",f"{out_filepath}{extra}") + else: + shutil.copy(f"{ROOT_DIR}/{category}/{filename}", f"{out_filepath}{extra}") + print(f"Copied '{title}' to {out_filepath}!") def check(args): success=0 @@ -590,16 +786,20 @@ def check(args): def delete(args): selection=search(args,True) for sel in selection: - if sel[0] != ".": + if sel[0] != "": try: category=sel[4] filename=sel[0] + fileuuid=os.path.splitext(filename)[0] + if etb.is_encrypted(fileuuid): + filename=f"{fileuuid}.enc" os.remove("{}/{}/{}".format(ROOT_DIR,category,filename)) except Exception as e: print(e) print("Couldn't delete a file!") return 1 tb.delete_index(sel) + print("Deleted '{}'!".format(sel[2])) def help(args): syntax=False @@ -610,11 +810,17 @@ def help(args): if re.match('[aA].*',arg): print("add:\tadds a new entry;\n\tInstant: image-index add <source> <tags> <content>") print("\tPrompt: image-index add") + print("Encryption: -e: Encrypt the added file\n\t -p: Do not encrypt the added file (plain)") print('EXAMPLES:\nimage-index add ~/Pictures/example.jpg "A Category" "Example file" "https://example.org" "Tag,Example,Some thing" "This is an example for the add option."') print("image-index add ~/Videos/movie.mp4 (This will ask for the other options in a prompt)\n") elif re.match('[mM].*',arg): - meta_help() - elif re.match('[cC].*',arg): + meta_help() + elif re.match('[cC].*', arg): + print("copy:\tcopies a file to a custom filepath based on a search query;\n\tInstant: image-index copy <filepath> <words/filters>") + print("\tPrompt: image-index copy") + print("EXAMPLE:\nimage-index copy ~/Pictures/Example.jpg -t example") + print("image-index copy ~/Pictures/ -t example (creates a file with the title and extension of the entry)\n") + elif re.match('[cC][hH].*',arg) or re.match('[cC].*[eE].*',arg): print("check:\tchecks the existence and correctness of all files in the index;\n\tSyntax: image-index check [options]") print("\tOptions: -v: show every faulty/orphaned entry") print("\t\t -f: check only if files exist (disables the other check)") @@ -623,10 +829,20 @@ def help(args): print("delete:\tdeletes a file and entry based on a search query;\n\tInstant: image-index delete <words/filters>") print("\tPrompt: image-index delete") print("EXAMPLE:\nimage-index delete -t Example -g Tag Example -a .mp4 add\n") + elif re.match('[iI].*',arg): + print("\timport:\tshows an add prompt for every file in a directory;\n\t\tSyntax: image-index import <directory path>") + print("EXAMPLE:\nimage-index import ~/Pictures/Vacation\n") elif re.match('[oO].*',arg): print("open:\topens a file based on a search query in the standard app;\n\tInstant: image-index open <words/filters>") print("\tPrompt: image-index open") print("EXAMPLE:\nimage-index open example -s example.org -i an example\n") + elif re.match('[rR].*',arg): + print("replaces a file of an entry based on a search query;\n\tInstant: image-index replace <replacement_file> <words/filters>") + print("\tPrompt: image-index replace") + print("EXAMPLE:\nimage-index replace ~/Pictures/example_new.jpg -f .jpg\n") + elif re.match('[sS][tT].*',arg) or re.match('[sS].*[aA].*',arg): + print("stats:\tshows some statistics about the index;\n\tSyntax: image-index stats [-v]") + print("EXAMPLE:\nimage-index stats -v (also shows all tags and categories)\n") elif re.match('[sS].*',arg): print("show:\tsearches through the index and shows the matches;\n\tInstant: image-index show <words/filters>") print("\tPrompt: image-index show") @@ -643,16 +859,33 @@ def help(args): print('image-index update tags -Tag -t Example (removes the tag "Tag")\n') if not args: print("SYNTAX: image-index <option> [args]") - print("OPTIONS:\n\thelp:\tdisplays this text") - print("\t\tadd one of the other options to see more info.") + print("OPTIONS:\n\thelp:\tdisplays helpful text\}n\t\tSyntax: image-index help [commands]") print("\tmeta:\tdisplays help for the metadata tables") print("\tadd:\tadds a new entry;\n\t\tSyntax: image-index add <filepath> <category> <title> <source> <tags> <content>") print("\tcheck:\tchecks the existence and correctness of all files in the index;\n\t\tSyntax: image-index check [options]") + print("\tcopy:\tcopies a file to a custom filepath based on a search query;\n\t\tSyntax: image-index copy <filepath> <words/filters>") print("\tdelete:\tdeletes a file and entry based on a search query;\n\t\tSyntax: image-index delete <words/filters>") + print("\timport:\tshows an add prompt for every file in a directory;\n\t\tSyntax: image-index import <directory path>") print("\topen:\topens a file based on a search query in the standard app;\n\t\tSyntax: image-index open <words/filters>") + print("\treplace:replaces a file of an entry based on a search query;\n\t\tSyntax: image-index replace <file> <words/filters>") print("\tshow:\tsearches through the index and shows the matches;\n\t\tSyntax: image-index show <words/filters>") + print("\tstats:\tshows some stats about the index;\n\t\tSyntax: image-index stats [-v]") print("\tupdate:\tchanges specific column based on a search query;\n\t\tSyntax: image-index update <column> <updated_value> <words/filters>") +def imports(args): + for arg in args: + if arg[-1] != "/": + arg+="/" + if os.path.exists(arg): + if not os.path.isfile(arg): + for sfile in os.listdir(arg): + if os.path.isfile(arg+sfile): + add(["{}{}".format(arg,sfile)]) + else: + print("Path '{}' is a file!".format(arg)) + else: + print("Path '{}' doesn't exist!".format(arg)) + def meta(args): if len(args) == 0 or re.match('[hH].*',args[0]): meta_help() @@ -675,7 +908,7 @@ def meta_check(typ,args): tres=ttb.check_index("TAGS") else: print("No valid type specified!") - return + return False if tres: yas=True print("Missing items:",','.join(tres)) @@ -684,16 +917,16 @@ def meta_check(typ,args): yas=False print("yo") for val in tres: - randhex=get_randhex() - if yas: eingabe=input("Enter Alias for {}: ".format(val.upper())) else: eingabe="" if re.match('[cC].*',typ): - ctb.add_index(val,eingabe,randhex) + ctb.add_index(val,eingabe) elif re.match('[tT].*',typ): - ttb.add_index(val,eingabe,randhex) + ttb.add_index(val,eingabe) + else: + print("Everything is good!") def meta_update(typ,args): if len(args) == 0: @@ -715,8 +948,8 @@ def meta_update(typ,args): success=ttb.update_index("ALIAS", update, "NAME", item[0]) else: print("The first argument needs to be either 'Category' or 'Tags'!") - return 1 - if item[0] != "." and success: + return False + if item[0] != "" and success: print("Updated {} to {}".format(alias,update)) def meta_help(): @@ -727,16 +960,22 @@ def meta_help(): print('\t\tExamples: image-index meta update tags Example "New alias"') print('\t\t\t image-index meta update category "A Category" "Example Category"\n') -def sopen(args): +def opens(args): plat=sys.platform selection=search(args,True) for sel in selection: - if not sel[0] == ".": - filename=sel[0] + if not sel[0] == "": + fileuuid=os.path.splitext(sel[0])[0] + if etb.is_encrypted(fileuuid): + filename=f"{fileuuid}.enc" + else: + filename=sel[0] category=sel[4] filepath="{}/{}/{}".format(ROOT_DIR,category,filename) else: continue + if etb.is_encrypted(fileuuid): + filepath=etb.decrypt(filepath) # temporary file in RAM if plat.startswith('linux'): subprocess.Popen([LINUX_APP_STARTER, filepath]) else: @@ -755,8 +994,25 @@ def update(args): typ=args[0] update=args[1] for sel in selection: - if sel[0] != ".": + if sel[0] != "": + n=0 + for i in tb.collist: + if i.lower() == typ.lower(): + old=sel[n] + else: + n+=1 tb.update_index(typ,update,sel) + if re.match('[tT][aA].*', typ) and re.match('[-+]', update[0]): + tags_list=[] + for tag in update[1:].split(","): + tags_list.append(tag) + tagstr=", ".join(tags_list) + if update[0] == "-": + print("Removed {}!".format(tagstr)) + elif update[0] == "+": + print("Added {}!".format(tagstr)) + else: + print("Updated {} to {}!".format(old,update)) def repair(err_list): sel_list=[] @@ -771,6 +1027,30 @@ def repair(err_list): os.remove(filepath) tb.delete_index(tup) +def replace(args): + if len(args) == 0: + filepath=input("Enter filepath of the replacement: ") + sel_list=search([]) + elif len(args) == 1: + filepath=args[0] + sel_list=search([]) + else: + filepath=args[0] + sel_list=search(args[1:]) + if len (sel_list) != 1: + print("Please select one entry for replacement!") + return False + item=sel_list[0] + if os.path.exists(filepath) and os.path.isfile(filepath): + title=item[2] + if tb.replace_file(filepath, item): + print(f"Replaced '{title}'!") + else: + print(f"Failed to replace '{title}'!") + else: + print("ERROR: Replacement doesn't exist or is not a file!") + return False + def search(args,quiet=False): if len(args) == 0: print("Separate the items with spaces.") @@ -781,52 +1061,95 @@ def search(args,quiet=False): res=tb.search_index(args.split(' '),quiet) else: print("\nQuery empty!") - return ["."] + return [""] else: res=tb.search_index(args,quiet) return res def show(args): tres=search(args,False) - if not tres[0] == ".": + if not tres[0] == "": for res in tres: print("Title: ",res[2]) print("\tSource:\t ",res[3]) alias=ctb.get_alias(res[4]) - if alias != ".": + if alias != "": print("\tCategory: {} ({})".format(alias,res[4])) else: print("\tCategory:",res[4]) - print("\tFilename:",res[0]) + print("\tFilename:",res[0],"(Encrypted)" if etb.is_encrypted(os.path.splitext(res[0])[0]) else "") print("\tHash:\t ",res[1]) tags_list=[] for tag in res[5].split(","): - name=ttb.get_alias(tag) - if name != ".": - tags_list.append(name) - else: - tags_list.append(tag) + alias=ttb.get_alias(tag) + if alias != "": + tags_list.append(alias) print("\tTags:\t ",",".join(tags_list)) print("\tContent: ",res[6]) +def stats(args): + verbose=False + for arg in args: + if re.match('[-]+[vV].*', arg): + verbose=True + entrynum=len(tb.get_col("HASH")) + encnum=len(etb.get_col("NAME")) + print(f"Entry count: {entrynum} ({encnum} encrypted)") + tags=ttb.get_col("NAME") + tagnum=len(tags) + print(f"Tags count: {tagnum}") + if verbose: + for item in tags: + name=item[0] + taguse=tb.get_item("TAGS", name) + if taguse[0] == "": + taguse=0 + else: + taguse=len(taguse) + print(f"\t{ttb.get_alias(name)} ({name}): Used by {taguse} entr%s"% ("y" if taguse == 1 else "ies")) + cats=ctb.get_col("NAME") + catnum=len(cats) + print(f"Categories count: {catnum}") + if verbose: + for item in cats: + name=item[0] + catuse=len(tb.get_item("CATEGORY", name)) + print(f"\t{ctb.get_alias(name)} ({name}): Used by {catuse} entr%s"% "y" if taguse == 1 else "ies") + + return True + def main(): if len(sys.argv) <= 1 or re.match('[hH].*',sys.argv[1]): help(sys.argv[2:]) else: command=sys.argv[1] args=[] - for i in sys.argv[2:]: - args.append(i) + for arg in sys.argv[2:]: + global bencrypt + if re.match("[-]{1,2}e.*",arg): # -e --encrypted + bencrypt=True + if re.match("[-]{1,2}p.*",arg): # -p --plain + bencrypt=False + else: + args.append(arg) if re.match('[aA].*',command): add(args) elif re.match('[mM].*',command): meta(args) - elif re.match('[cC].*',command): + elif re.match('[cC][hH].*',command) or re.match('[cC].*[eE].*',command): check(args) + elif re.match('[cC].*',command): + copy(args) elif re.match('[dD].*',command): delete(args) + elif re.match('[iI].*',command): + imports(args) elif re.match('[oO].*',command): - sopen(args) + opens(args) + elif re.match('[rR].*',command): + replace(args) + elif re.match('[sS][tT].*',command) or re.match('[sS].*[aA].*',command): + stats(args) elif re.match('[sS].*',command): show(args) elif re.match('[uU].*',command): @@ -835,11 +1158,14 @@ def main(): print("No such option!") tb.connection.close() ctb.connection.close() + etb.connection.close() ttb.connection.close() #input("Press return...") if __name__ == "__main__": - filepath=CONFIG_DIR + '/index.db' + bencrypt=ENCRYPT # boolean for encryption in session + filepath=INDEX_FILE + etb = enctable(filepath) tb = filestable(filepath) ctb = metatable("CATEGORY",filepath) ttb = metatable("TAGS",filepath)