Compare commits

...

10 commits

Author SHA1 Message Date
marcel 8a3822b476 changed hostname in README 2023-07-28 15:09:30 +00:00
Michael Rodin cabceaa7fd Limit entries in selection 2023-02-15 17:11:00 +01:00
Michael Rodin bc2e49a3ea added stats function 2023-01-18 09:17:23 +01:00
Michael Rodin 0b6eb07e82 added encryption, copy and replace. 2023-01-17 18:10:24 +01:00
Michael Rodin ff62762484 better code 2022-12-08 15:00:53 +01:00
Michael Rodin 35a58d4952 added import function 2022-12-05 19:52:07 +01:00
Michael Rodin 291fffe057 If specific meta search finds nothing, make unspecfic meta search. 2022-12-02 16:42:40 +01:00
Michael Rodin 527f006e6b Simpler and safer get_randhex() integration 2022-12-02 11:58:11 +01:00
Michael Rodin 3f62f678bd Added text to indicate successful execution 2022-12-01 18:20:05 +01:00
Michael Rodin 26744964f2 improved help 2022-11-29 11:04:42 +01:00
2 changed files with 468 additions and 128 deletions

View file

@ -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.
* 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.

View file

@ -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:
if not i in res:
res.append(i)
m+=1
n+=1
# if the table is empty, return ".".
# 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]
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])
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 update_index(self,typ,update,sel_list):
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 <filepath> <category> <title> <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):
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] == ".":
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)