Compare commits

..

No commits in common. "8a3822b476f49f995931a6ea8699c3aaeef6223f" and "c8d306dc6131b62025514dd2a0811157fc1373ee" have entirely different histories.

2 changed files with 126 additions and 466 deletions

View file

@ -1,39 +1,25 @@
# image-index # 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. 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. 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 ## Functions
* add - Add a file and entry to the index * 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 * check - check if all files saved in the index exist and aren't faulty
* delete - delete a file and remove the entry * delete - delete a file and remove the entry
* import - lets you add every file to the index in a chosen directory * meta - a command to change aliases of categorties and tags
* 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) * 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 * 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 * update - change a value of an entry in the index or move a file to another category
## Dependencies ## Installation
* hashlib
* pycryptodomex
* sqlite3
## Downloading
```sh ```sh
git clone https://git.marcelsite.com/marcel/image-index-py.git git clone https://gitlab.com/rodin_schule/image-index-py.git
cd ./image-index-py cd ./image-index-py
chmod u+x image-index
``` ```
## Configuration ## Configuration
The top of the file holds some very important variables that need to be looked at by the user: 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) * ROOT_DIR: The absolute path of where you want to save your files (the directories for the categories will be created there)
* INDEX_FILE: The absolute path of the database file. * 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`. * 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.

View file

@ -1,18 +1,15 @@
#!/usr/bin/python3 #!/bin/python3
import ast,hashlib,os,random,re,secrets,shutil,subprocess,sys,tempfile import hashlib,os,random,re,shutil,subprocess,sys
from pathlib import Path
from uuid import uuid4 from uuid import uuid4
import sqlite3 as sql 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 ROOT_DIR=os.getcwd() # The directory where all directories and files of the index are located
INDEX_FILE=ROOT_DIR+"/index.db" # The database file 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 application LINUX_APP_STARTER="xdg-open" # The command which opens the files in the default applications
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(): class database():
def __init__(self,filepath = INDEX_FILE): def __init__(self,filepath = CONFIG_DIR + "/index.db"):
self.connection=None self.connection=None
self.crsr=None self.crsr=None
if not os.path.exists(filepath) : if not os.path.exists(filepath) :
@ -21,11 +18,10 @@ class database():
self.connection = sql.connect(filepath) self.connection = sql.connect(filepath)
self.crsr = self.connection.cursor() self.crsr = self.connection.cursor()
def add_index(self,vallist,collist=""): def add_index(self,vallist):
collist=self.collist if not collist else collist
# compile the options into a command for the SQLite database # compile the options into a command for the SQLite database
colstring=",".join(collist) colstring=",".join(self.collist)
valstring='"{}"'.format('","'.join(vallist)) valstring="'{}'".format("','".join(vallist))
self.crsr.execute("""INSERT INTO {table} ({cols}) self.crsr.execute("""INSERT INTO {table} ({cols})
VALUES ({vals}); VALUES ({vals});
""".format(table=self.name,cols=colstring,vals=valstring)) """.format(table=self.name,cols=colstring,vals=valstring))
@ -47,25 +43,21 @@ class database():
self.crsr.execute(sqlcommand) self.crsr.execute(sqlcommand)
sqlcommand = """CREATE TABLE CATEGORY( sqlcommand = """CREATE TABLE CATEGORY(
NAME TEXT PRIMARY KEY NOT NULL, NAME TEXT PRIMARY KEY NOT NULL,
ALIAS TEXT ALIAS TEXT,
DESCRIPTION TEXT
); """ ); """
self.crsr.execute(sqlcommand) self.crsr.execute(sqlcommand)
sqlcommand = """CREATE TABLE TAGS( sqlcommand = """CREATE TABLE TAGS(
NAME TEXT PRIMARY KEY NOT NULL, NAME TEXT PRIMARY KEY NOT NULL,
ALIAS TEXT ALIAS TEXT,
); """ DESCRIPTION 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) self.crsr.execute(sqlcommand)
def delete_index(self,typ,item): def delete_index(self,typ,item):
self.crsr.execute("DELETE FROM {} WHERE {}='{}'".format(self.name,typ,item)) self.crsr.execute("DELETE FROM {} WHERE {}='{}'".format(self.name,typ,item))
self.connection.commit() self.connection.commit()
return True return
def get_col(self,column = "*"): def get_col(self,column = "*"):
# get the column of some table. If no options given, return all columns # get the column of some table. If no options given, return all columns
@ -74,9 +66,9 @@ class database():
res=[] res=[]
for i in tres: for i in tres:
res.append(i) res.append(i)
# if the table is empty, return "". # if the table is empty, return ".".
if not res: if not res:
res="" res="."
return res return res
def get_item(self,column,where,specific=False): def get_item(self,column,where,specific=False):
@ -102,11 +94,17 @@ class database():
n=0 n=0
res=[] res=[]
for i in tres: for i in tres:
if not i in res: m=0
res.append(i) for j in tres:
# if the table is empty, return "". if i in j and n!=m:
continue
else:
res.append(i)
m+=1
n+=1
# if the table is empty, return ".".
if not res: if not res:
return [""] return ["."]
return res return res
def select_index(self,sel_list,quiet=False): def select_index(self,sel_list,quiet=False):
@ -117,7 +115,7 @@ class database():
if len(sel_list) > 1: if len(sel_list) > 1:
n=0 n=0
print("Found several matches:") print("Found several matches:")
for tup in sel_list[:MAX_ITEMS]: for tup in sel_list:
temp_list=[] temp_list=[]
for j in tup: for j in tup:
temp_list.append(j) temp_list.append(j)
@ -125,7 +123,7 @@ class database():
if self.name == "FILES": if self.name == "FILES":
print("\tTitle:\t ",temp_list[2]) print("\tTitle:\t ",temp_list[2])
name=ctb.get_alias(temp_list[4]) name=ctb.get_alias(temp_list[4])
if name != "": if name != ".":
category=name category=name
else: else:
category=temp_list[4] category=temp_list[4]
@ -133,7 +131,7 @@ class database():
tags_list=[] tags_list=[]
for tag in temp_list[5].split(","): for tag in temp_list[5].split(","):
name=ttb.get_alias(tag) name=ttb.get_alias(tag)
if name != "": if name != ".":
tag=name tag=name
tags_list.append(tag) tags_list.append(tag)
print("\tTags:\t ",",".join(tags_list)) print("\tTags:\t ",",".join(tags_list))
@ -142,11 +140,9 @@ class database():
print("\tAlias:\t ",temp_list[1]) print("\tAlias:\t ",temp_list[1])
print("\tDescription: ",temp_list[2]) print("\tDescription: ",temp_list[2])
n+=1 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)) eingabe=input("Enter number(s) (0-{}; '*' for all entries): ".format(n-1))
if not eingabe: if not eingabe:
return [""] return ["."]
num_list=[] num_list=[]
if re.match('[*]',eingabe): if re.match('[*]',eingabe):
for i in range(0,n): for i in range(0,n):
@ -163,10 +159,10 @@ class database():
if not quiet: if not quiet:
print("\nFinal match{}:".format("es" if len(num_list)-nminus > 1 else "")) print("\nFinal match{}:".format("es" if len(num_list)-nminus > 1 else ""))
else: else:
if sel_list[0] == "": if sel_list[0] == ".":
if not quiet == "strict": if not quiet == "strict":
print("No matching entry found!") print("No matching entry found!")
return [""] return ["."]
res=sel_list res=sel_list
if not quiet == "strict": if not quiet == "strict":
print("\nMatch found!") print("\nMatch found!")
@ -182,12 +178,10 @@ class database():
if not secondlist: if not secondlist:
n=0 n=0
for i in self.get_col(typ): for i in self.get_col(typ):
if len(temp_list) > MAX_ITEMS:
break
aliases=[] aliases=[]
if i == "" and self.name == "FILES": if i == "." and self.name == "FILES":
print("NO ENTRIES IN THE INDEX!") print("NO ENTRIES IN THE INDEX!")
return "" return "."
# get aliases for the checks, but only for unspecific search! # get aliases for the checks, but only for unspecific search!
if self.name == "FILES": if self.name == "FILES":
if typ == "*": if typ == "*":
@ -228,7 +222,7 @@ class database():
n+=1 n+=1
else: else:
if not secondlist[0] == "": if not secondlist[0] == ".":
for i in secondlist: for i in secondlist:
for j in firstlist: for j in firstlist:
if j in i: if j in i:
@ -237,8 +231,8 @@ class database():
else: else:
return secondlist return secondlist
if not temp_list: if not temp_list:
return [""] return ["."]
return temp_list[:MAX_ITEMS+1] return temp_list
return secondlist return secondlist
def update_index(self,typ,update,where,val): def update_index(self,typ,update,where,val):
@ -246,167 +240,50 @@ class database():
self.connection.commit() self.connection.commit()
return True return True
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 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): class metatable(database):
def __init__(self,typ,filepath = INDEX_FILE): def __init__(self,typ,filepath = CONFIG_DIR + "/index.db"):
self.name=typ.upper() self.name=typ.upper()
self.collist=["NAME","ALIAS"] self.collist=["NAME","ALIAS","DESCRIPTION"]
super().__init__(filepath) super().__init__(filepath)
def add_index(self,val,alias,randhex=None): def add_index(self,val,alias,randhex=""):
if not randhex: super().add_index([val.lower() + "-" + randhex,alias,''])
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): def check_index(self,typ):
res=[] res=[]
for i in tb.get_col(typ): for i in tb.get_col(typ):
success=0 success=0
for j in self.get_col("NAME"): for j in self.get_col("NAME"):
if j[0].split(".")[0] in i[0]: if j[0] in i[0].lower():
success=1 success=1
if not i[0] in res and success == 0: if not i[0] in res and success == 0:
res.append(i[0]) res.append(i[0])
return res return res
def get_alias(self,arg): def get_alias(self,arg):
if not arg:
return ""
selection=self.search_index(arg,"strict") selection=self.search_index(arg,"strict")
item=selection[0] item=selection[0]
if item[0] != "": if item[0] != ".":
alias=item[1] alias=item[1]
else: else:
alias = "" alias = "."
return alias return alias
def get_name(self,arg): def get_name(self,arg):
if not arg:
return ""
selection=self.search_index(arg,"strict") selection=self.search_index(arg,"strict")
item=selection[0] item=selection[0]
if not item: name=item[0]
name=""
else:
name=item[0]
return name return name
def search_index(self,args,quiet=True): def search_index(self,args,quiet=True):
selection=[] selection=[]
selection=self.sql_compare_list("*", [args], selection,True) 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) selection=self.select_index(selection,quiet)
return selection return selection
def update_index(self, typ, update, where, val): def update_index(self, typ, update, where, val):
selection=self.search_index(update,"strict") 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)) print("One entry is already called {}!".format(update))
return False return False
else: else:
@ -414,7 +291,7 @@ class metatable(database):
return True return True
class filestable(database): class filestable(database):
def __init__(self,filepath = INDEX_FILE): def __init__(self,filepath = CONFIG_DIR + "/index.db"):
self.name="FILES" self.name="FILES"
self.collist=["FILE","HASH","TITLE","SOURCE","CATEGORY","TAGS","CONTENT"] self.collist=["FILE","HASH","TITLE","SOURCE","CATEGORY","TAGS","CONTENT"]
super().__init__(filepath) super().__init__(filepath)
@ -422,128 +299,88 @@ class filestable(database):
def add_index(self,filepath,category="default",title="",source="",tags="",content=""): def add_index(self,filepath,category="default",title="",source="",tags="",content=""):
filehash=self.get_hash(filepath) # make hash of file before copy filehash=self.get_hash(filepath) # make hash of file before copy
if filehash in str(self.get_col("HASH")): if filehash in str(self.get_col("HASH")):
print("This file already has an entry!") print("This file already exists!")
return False return
n=0 n=0
randhex=get_randhex()
# get the name of the category from the meta table # get the name of the category from the meta table
if not category: if not category:
category="default" category="default"
name=ctb.get_name(category) name=ctb.get_name(category)
if name: if name != ".":
category=name category=name
else: else:
ctb.add_index(category.lower(),category) ctb.add_index(category.lower(),category,randhex)
category=ctb.get_name(category) category=ctb.get_name(category)
# get the name of the tags from the meta table # get the name of the tags from the meta table
tags_list=[] tags_list=[]
for tag in tags.split(','): for tag in tags.split(','):
randhex=get_randhex()
name=ttb.get_name(tag) name=ttb.get_name(tag)
if name != "": if name != ".":
tag=name tag=name
else: else:
ttb.add_index(tag.lower(),tag) ttb.add_index(tag.lower(),tag,randhex)
tags_list.append(ttb.get_name(tag)) tags_list.append(ttb.get_name(tag))
tags=",".join(tags_list) tags=",".join(tags_list)
fileext=os.path.splitext(filepath)[-1] filetype=os.path.splitext(filepath)[1]
fileuuid=str(uuid4()) filename=str(uuid4()) + filetype
filename=fileuuid+fileext
if not os.path.exists("{}/{}".format(ROOT_DIR,category)): if not os.path.exists("{}/{}".format(ROOT_DIR,category)):
os.makedirs("{}/{}".format(ROOT_DIR,category)) os.makedirs("{}/{}".format(ROOT_DIR,category))
# try to copy the file, return if error. # try to copy the file, return if error.
try: try:
if bencrypt: shutil.copy(filepath,"{}/{}/{}".format(ROOT_DIR,category,filename))
etb.encrypt(filepath, f"{ROOT_DIR}/{category}/{fileuuid}.enc")
else:
shutil.copy(filepath,f"{ROOT_DIR}/{category}/{filename}")
except Exception as e: except Exception as e:
print(e) print(e)
print("COULDN'T COPY FILE TO DESTINATION!") print("COULDN'T COPY FILE TO DESTINATION!")
return False return
vallist=[filename,filehash,title,source,category,tags,content] vallist=[filename,filehash,title,source,category,tags,content]
super().add_index(vallist) super().add_index(vallist)
return True
def check_index(self): def check_index(self):
hash_list=[] hash_list=[]
path_list=[] path_list=[]
enc_num=0
for i in self.get_col(): for i in self.get_col():
filename=i[0] filename=i[0]
fileuuid=filename.split(".")[0]
category=i[4] category=i[4]
filehash1=i[1] filehash1=i[1]
if etb.is_encrypted(fileuuid):
enc_num+=1
filename=f"{fileuuid}.enc"
filepath="{}/{}/{}".format(ROOT_DIR,category,filename) filepath="{}/{}/{}".format(ROOT_DIR,category,filename)
if not os.path.exists(filepath): if not os.path.exists(filepath):
path_list.append(i) path_list.append(i)
if etb.is_encrypted(fileuuid):
continue
try: try:
filehash2=self.get_hash(filepath) filehash2=self.get_hash(filepath)
if not filehash1 == filehash2: if not filehash1 == filehash2:
hash_list.append(i) hash_list.append(i)
except Exception: except Exception:
path_list.append(i) path_list.append(i)
print(f"Encrypted files: {enc_num}; hashes not checked.")
return hash_list,path_list return hash_list,path_list
def delete_index(self,sel_list): def delete_index(self,sel_list):
item_list=self.get_item(self.collist[1],sel_list[1]) self.get_item("HASH",sel_list[1])
if len(item_list) == 1: super().delete_index("HASH",sel_list[1])
item=item_list[0]
category=item[4] def update_index(self,typ,update,sel_list):
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() typ=typ.upper()
if typ in ["FILE","HASH"] and not omnipotent: if typ in ["FILE","HASH"]:
print("This type can't be changed!") print("This type can't be changed!")
return False return 1
category=sel_list[4] category=sel_list[4]
filehash=sel_list[1] filehash=sel_list[1]
filename=sel_list[0] filename=sel_list[0]
fileuuid=sel_list[0].split(".")[0] randhex=get_randhex()
if typ in ["CATEGORY"]: if typ in ["CATEGORY"]:
# get alias of category # get alias of category
name=ctb.get_name(update) name=ctb.get_name(update)
if name != "": if name != ".":
update=name update=name
else: else:
ctb.add_index(update.lower(),update) ctb.add_index(update.lower(),update,randhex)
update=ctb.get_name(update) update=ctb.get_name(update)
if not os.path.exists("{}/{}".format(ROOT_DIR,update)): if not os.path.exists("{}/{}".format(ROOT_DIR,update)):
os.makedirs("{}/{}".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)) shutil.move("{}/{}/{}".format(ROOT_DIR,category,filename), "{}/{}/{}".format(ROOT_DIR,update,filename))
if typ in ["TAGS"]: if typ in ["TAGS"]:
tags_list=[] tags_list=[]
@ -551,9 +388,10 @@ class filestable(database):
for tag in sel_list[5].split(","): for tag in sel_list[5].split(","):
tags_list.append(tag) tags_list.append(tag)
for tag in update[1:].split(","): for tag in update[1:].split(","):
randhex=get_randhex()
name=ttb.get_name(tag) name=ttb.get_name(tag)
if name == "": if name == ".":
ttb.add_index(tag.lower(), tag) ttb.add_index(tag.lower(), tag, randhex)
name=ttb.get_name(tag) name=ttb.get_name(tag)
tags_list.append(name) tags_list.append(name)
elif update[0][0] == "-": elif update[0][0] == "-":
@ -561,7 +399,8 @@ class filestable(database):
success=0 success=0
for i in update[1:].split(","): for i in update[1:].split(","):
name=ttb.get_name(i) name=ttb.get_name(i)
if name == "": if name == ".":
randhex=get_randhex()
ttb.add_index(tag.lower(), tag) ttb.add_index(tag.lower(), tag)
name=ttb.get_name(tag) name=ttb.get_name(tag)
if name == tag: if name == tag:
@ -570,11 +409,12 @@ class filestable(database):
tags_list.append(tag) tags_list.append(tag)
else: else:
for tag in update.split(","): for tag in update.split(","):
randhex=get_randhex()
name=ttb.get_name(tag) name=ttb.get_name(tag)
if name != "": if name != ".":
tags_list.append(name) tags_list.append(name)
else: else:
ttb.add_index(tag.lower(), tag) ttb.add_index(tag.lower(), tag, randhex)
tags_list.append(ttb.get_name(tag)) tags_list.append(ttb.get_name(tag))
update=",".join(tags_list) update=",".join(tags_list)
super().update_index(typ, update, "HASH", filehash) super().update_index(typ, update, "HASH", filehash)
@ -627,7 +467,7 @@ class filestable(database):
alle.append(arg) alle.append(arg)
elif snext == "category": elif snext == "category":
name=ctb.get_name(arg) name=ctb.get_name(arg)
if name != "": if name != ".":
arg=name arg=name
category.append(arg) category.append(arg)
elif snext == "file": elif snext == "file":
@ -642,13 +482,13 @@ class filestable(database):
'''if "," in arg: '''if "," in arg:
for tag in arg.split(","): for tag in arg.split(","):
name=ttb.get_name(arg) name=ttb.get_name(arg)
if name != "": if name != ".":
arg=name arg=name
tags.append(arg) tags.append(arg)
else: else:
name=ttb.get_name(arg) name=ttb.get_name(arg)
print("name",name) print("name",name)
if name != "": if name != ".":
arg=name''' arg=name'''
tags.append(arg) tags.append(arg)
else: else:
@ -666,10 +506,10 @@ class filestable(database):
return self.select_index(selection,quiet) return self.select_index(selection,quiet)
def get_randchar(count=5): def get_randhex(count=5):
randhex="" randhex=""
for i in range(count): for i in range(count):
randhex+=random.choice("0123456789abcdefghijklmnopqrstuvwxyz") randhex+=random.choice("0123456789abcdef")
return randhex return randhex
def add(args): def add(args):
@ -678,64 +518,28 @@ def add(args):
return return
n=0 n=0
for i in ["Filepath","Category","Title","Source","Tags","Content"]: for i in ["Filepath","Category","Title","Source","Tags","Content"]:
i=i.title()
try: try:
print("{}: {}".format(i,args[n])) print("{}: {}".format(i,args[n]))
except Exception: except Exception:
extra=""
if i == "Tags": if i == "Tags":
extra=" (Separate with ',')" extra=" (Separate with ',')"
eingabe = input("{}{}: ".format(i,extra)) else:
extra=""
eingabe = input("Enter {}{}: ".format(i,extra))
if i in ["Category"] and not eingabe: if i in ["Category"] and not eingabe:
print("{} set to 'default'".format(i)) print("{} set to 'default'".format(i))
eingabe="default" eingabe="default"
args.append(eingabe) args.append(eingabe)
if i in ["Filepath"]: 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]: if not args[n]:
print("{} must not be empty!".format(i)) print("{} must not be empty!".format(i))
return False return 1
else: else:
if not os.path.isfile(args[n]): if not Path(args[n]).is_file():
print(" The file '{}' doesn't exist or is not a file!".format(args[n])) print(" The file '{}' doesn't exist or is not a file!".format(eingabe))
return False return 1
n+=1 n+=1
success=tb.add_index(args[0],args[1],args[2],args[3],args[4],args[5]) 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): def check(args):
success=0 success=0
@ -786,20 +590,16 @@ def check(args):
def delete(args): def delete(args):
selection=search(args,True) selection=search(args,True)
for sel in selection: for sel in selection:
if sel[0] != "": if sel[0] != ".":
try: try:
category=sel[4] category=sel[4]
filename=sel[0] 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)) os.remove("{}/{}/{}".format(ROOT_DIR,category,filename))
except Exception as e: except Exception as e:
print(e) print(e)
print("Couldn't delete a file!") print("Couldn't delete a file!")
return 1 return 1
tb.delete_index(sel) tb.delete_index(sel)
print("Deleted '{}'!".format(sel[2]))
def help(args): def help(args):
syntax=False syntax=False
@ -810,17 +610,11 @@ def help(args):
if re.match('[aA].*',arg): if re.match('[aA].*',arg):
print("add:\tadds a new entry;\n\tInstant: image-index add <filepath> <category> <title> <source> <tags> <content>") print("add:\tadds a new entry;\n\tInstant: image-index add <filepath> <category> <title> <source> <tags> <content>")
print("\tPrompt: image-index add") 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('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") print("image-index add ~/Videos/movie.mp4 (This will ask for the other options in a prompt)\n")
elif re.match('[mM].*',arg): elif re.match('[mM].*',arg):
meta_help() meta_help()
elif re.match('[cC].*', arg): 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("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("\tOptions: -v: show every faulty/orphaned entry")
print("\t\t -f: check only if files exist (disables the other check)") print("\t\t -f: check only if files exist (disables the other check)")
@ -829,20 +623,10 @@ def help(args):
print("delete:\tdeletes a file and entry based on a search query;\n\tInstant: image-index delete <words/filters>") 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("\tPrompt: image-index delete")
print("EXAMPLE:\nimage-index delete -t Example -g Tag Example -a .mp4 add\n") 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): 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("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("\tPrompt: image-index open")
print("EXAMPLE:\nimage-index open example -s example.org -i an example\n") 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): elif re.match('[sS].*',arg):
print("show:\tsearches through the index and shows the matches;\n\tInstant: image-index show <words/filters>") print("show:\tsearches through the index and shows the matches;\n\tInstant: image-index show <words/filters>")
print("\tPrompt: image-index show") print("\tPrompt: image-index show")
@ -859,33 +643,16 @@ def help(args):
print('image-index update tags -Tag -t Example (removes the tag "Tag")\n') print('image-index update tags -Tag -t Example (removes the tag "Tag")\n')
if not args: if not args:
print("SYNTAX: image-index <option> [args]") print("SYNTAX: image-index <option> [args]")
print("OPTIONS:\n\thelp:\tdisplays helpful text\}n\t\tSyntax: image-index help [commands]") print("OPTIONS:\n\thelp:\tdisplays this text")
print("\t\tadd one of the other options to see more info.")
print("\tmeta:\tdisplays help for the metadata tables") 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("\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("\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("\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("\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("\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>") 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): def meta(args):
if len(args) == 0 or re.match('[hH].*',args[0]): if len(args) == 0 or re.match('[hH].*',args[0]):
meta_help() meta_help()
@ -908,7 +675,7 @@ def meta_check(typ,args):
tres=ttb.check_index("TAGS") tres=ttb.check_index("TAGS")
else: else:
print("No valid type specified!") print("No valid type specified!")
return False return
if tres: if tres:
yas=True yas=True
print("Missing items:",','.join(tres)) print("Missing items:",','.join(tres))
@ -917,16 +684,16 @@ def meta_check(typ,args):
yas=False yas=False
print("yo") print("yo")
for val in tres: for val in tres:
randhex=get_randhex()
if yas: if yas:
eingabe=input("Enter Alias for {}: ".format(val.upper())) eingabe=input("Enter Alias for {}: ".format(val.upper()))
else: else:
eingabe="" eingabe=""
if re.match('[cC].*',typ): if re.match('[cC].*',typ):
ctb.add_index(val,eingabe) ctb.add_index(val,eingabe,randhex)
elif re.match('[tT].*',typ): elif re.match('[tT].*',typ):
ttb.add_index(val,eingabe) ttb.add_index(val,eingabe,randhex)
else:
print("Everything is good!")
def meta_update(typ,args): def meta_update(typ,args):
if len(args) == 0: if len(args) == 0:
@ -948,8 +715,8 @@ def meta_update(typ,args):
success=ttb.update_index("ALIAS", update, "NAME", item[0]) success=ttb.update_index("ALIAS", update, "NAME", item[0])
else: else:
print("The first argument needs to be either 'Category' or 'Tags'!") print("The first argument needs to be either 'Category' or 'Tags'!")
return False return 1
if item[0] != "" and success: if item[0] != "." and success:
print("Updated {} to {}".format(alias,update)) print("Updated {} to {}".format(alias,update))
def meta_help(): def meta_help():
@ -960,22 +727,16 @@ def meta_help():
print('\t\tExamples: image-index meta update tags Example "New alias"') 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') print('\t\t\t image-index meta update category "A Category" "Example Category"\n')
def opens(args): def sopen(args):
plat=sys.platform plat=sys.platform
selection=search(args,True) selection=search(args,True)
for sel in selection: for sel in selection:
if not sel[0] == "": if not sel[0] == ".":
fileuuid=os.path.splitext(sel[0])[0] filename=sel[0]
if etb.is_encrypted(fileuuid):
filename=f"{fileuuid}.enc"
else:
filename=sel[0]
category=sel[4] category=sel[4]
filepath="{}/{}/{}".format(ROOT_DIR,category,filename) filepath="{}/{}/{}".format(ROOT_DIR,category,filename)
else: else:
continue continue
if etb.is_encrypted(fileuuid):
filepath=etb.decrypt(filepath) # temporary file in RAM
if plat.startswith('linux'): if plat.startswith('linux'):
subprocess.Popen([LINUX_APP_STARTER, filepath]) subprocess.Popen([LINUX_APP_STARTER, filepath])
else: else:
@ -994,25 +755,8 @@ def update(args):
typ=args[0] typ=args[0]
update=args[1] update=args[1]
for sel in selection: 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) 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): def repair(err_list):
sel_list=[] sel_list=[]
@ -1027,30 +771,6 @@ def repair(err_list):
os.remove(filepath) os.remove(filepath)
tb.delete_index(tup) 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): def search(args,quiet=False):
if len(args) == 0: if len(args) == 0:
print("Separate the items with spaces.") print("Separate the items with spaces.")
@ -1061,95 +781,52 @@ def search(args,quiet=False):
res=tb.search_index(args.split(' '),quiet) res=tb.search_index(args.split(' '),quiet)
else: else:
print("\nQuery empty!") print("\nQuery empty!")
return [""] return ["."]
else: else:
res=tb.search_index(args,quiet) res=tb.search_index(args,quiet)
return res return res
def show(args): def show(args):
tres=search(args,False) tres=search(args,False)
if not tres[0] == "": if not tres[0] == ".":
for res in tres: for res in tres:
print("Title: ",res[2]) print("Title: ",res[2])
print("\tSource:\t ",res[3]) print("\tSource:\t ",res[3])
alias=ctb.get_alias(res[4]) alias=ctb.get_alias(res[4])
if alias != "": if alias != ".":
print("\tCategory: {} ({})".format(alias,res[4])) print("\tCategory: {} ({})".format(alias,res[4]))
else: else:
print("\tCategory:",res[4]) print("\tCategory:",res[4])
print("\tFilename:",res[0],"(Encrypted)" if etb.is_encrypted(os.path.splitext(res[0])[0]) else "") print("\tFilename:",res[0])
print("\tHash:\t ",res[1]) print("\tHash:\t ",res[1])
tags_list=[] tags_list=[]
for tag in res[5].split(","): for tag in res[5].split(","):
alias=ttb.get_alias(tag) name=ttb.get_alias(tag)
if alias != "": if name != ".":
tags_list.append(alias) tags_list.append(name)
else:
tags_list.append(tag)
print("\tTags:\t ",",".join(tags_list)) print("\tTags:\t ",",".join(tags_list))
print("\tContent: ",res[6]) 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(): def main():
if len(sys.argv) <= 1 or re.match('[hH].*',sys.argv[1]): if len(sys.argv) <= 1 or re.match('[hH].*',sys.argv[1]):
help(sys.argv[2:]) help(sys.argv[2:])
else: else:
command=sys.argv[1] command=sys.argv[1]
args=[] args=[]
for arg in sys.argv[2:]: for i in sys.argv[2:]:
global bencrypt args.append(i)
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): if re.match('[aA].*',command):
add(args) add(args)
elif re.match('[mM].*',command): elif re.match('[mM].*',command):
meta(args) meta(args)
elif re.match('[cC][hH].*',command) or re.match('[cC].*[eE].*',command):
check(args)
elif re.match('[cC].*',command): elif re.match('[cC].*',command):
copy(args) check(args)
elif re.match('[dD].*',command): elif re.match('[dD].*',command):
delete(args) delete(args)
elif re.match('[iI].*',command):
imports(args)
elif re.match('[oO].*',command): elif re.match('[oO].*',command):
opens(args) sopen(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): elif re.match('[sS].*',command):
show(args) show(args)
elif re.match('[uU].*',command): elif re.match('[uU].*',command):
@ -1158,14 +835,11 @@ def main():
print("No such option!") print("No such option!")
tb.connection.close() tb.connection.close()
ctb.connection.close() ctb.connection.close()
etb.connection.close()
ttb.connection.close() ttb.connection.close()
#input("Press return...") #input("Press return...")
if __name__ == "__main__": if __name__ == "__main__":
bencrypt=ENCRYPT # boolean for encryption in session filepath=CONFIG_DIR + '/index.db'
filepath=INDEX_FILE
etb = enctable(filepath)
tb = filestable(filepath) tb = filestable(filepath)
ctb = metatable("CATEGORY",filepath) ctb = metatable("CATEGORY",filepath)
ttb = metatable("TAGS",filepath) ttb = metatable("TAGS",filepath)