From 4266ae5603631703c0bf39f618caf271c54e87a1 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:03:01 -0500 Subject: [PATCH 01/22] feat: has 3 new arguments , and --- spacesavers2_blamematrix | 52 ++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/spacesavers2_blamematrix b/spacesavers2_blamematrix index 20cf569..9885c7c 100755 --- a/spacesavers2_blamematrix +++ b/spacesavers2_blamematrix @@ -28,7 +28,7 @@ def main(): Version: {} Example: - > spacesavers2_blamematrix -f /output/from/spacesavers2_finddup/prefix.allusers.files.gz -d 3 + > spacesavers2_blamematrix -f /output/from/spacesavers2_mimeo/prefix.allusers.mimeo.files.gz -d 3 -o prefix.blamematrix.tsv """.format( __version__ ) @@ -46,7 +46,7 @@ def main(): required=True, type=str, default=sys.stdin, - help="spacesavers2_mimeo prefix.allusers.files.gz file", + help="spacesavers2_mimeo prefix.allusers.mimeo.files.gz file", ) parser.add_argument( "-l", @@ -57,6 +57,30 @@ def main(): default=3, help="folder level to use for creating matrix", ) + parser.add_argument( + "-r", + "--humanreable", + dest="humanreable", + required=False, + action=argparse.BooleanOptionalAction, + help="sizes are printed in human readable format ... (default: Bytes)", + ) + parser.add_argument( + "-z", + "--includezeros", + dest="includezeros", + required=False, + action=argparse.BooleanOptionalAction, + help="include folders where totalbytes is zero.", + ) + parser.add_argument( + "-o", + "--outfile", + dest="outfile", + required=False, + type=str, + help="output tab-delimited file (default STDOUT)", + ) parser.add_argument("-v", "--version", action="version", version=__version__) print_with_timestamp( @@ -85,20 +109,34 @@ def main(): blamematrix[user][folder] += dfu.bm[user][folder] blamematrix["allusers"][folder] += dfu.bm[user][folder] + if args.outfile: + of = open(args.outfile, "w") + else: + of = sys.stdout + users = list(blamematrix.keys()) folders = list(blamematrix["allusers"].keys()) users2 = ["folder"] users2.extend(users) - print("\t".join(users2)) + outstr = "\t".join(users2) + of.write("%s\n"%(outstr)) for folder in folders: - print(folder, end="") + outlist = [] + outlist.append(str(folder)) for user in users: try: - hrsize = get_human_readable_size(blamematrix[user][folder]) + hrsize = blamematrix[user][folder] + if args.humanreable: + hrsize = get_human_readable_size(hrsize) except KeyError: hrsize = "0" - print("\t{}".format(hrsize), end="") - print("") + outlist.append(str(hrsize)) + if blamematrix["allusers"][folder] == 0 : + if args.includezeros: + of.write("%s\n"%("\t".join(outlist))) + else: + of.write("%s\n"%("\t".join(outlist))) + if args.outfile: of.close() print_with_timestamp(start=start, scriptname=scriptname, string="Done!") From 37f63015be4edb6dfc86b7d8778e48e41ceba423 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:04:30 -0500 Subject: [PATCH 02/22] feat: adding '--outfile' argument --- spacesavers2_grubbers | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/spacesavers2_grubbers b/spacesavers2_grubbers index 77a4709..5bbd2a5 100755 --- a/spacesavers2_grubbers +++ b/spacesavers2_grubbers @@ -46,16 +46,24 @@ def main(): required=True, type=str, default=sys.stdin, - help="spacesavers2_mimeo prefix..files.gz file", + help="spacesavers2_mimeo prefix..mimeo.files.gz file", ) parser.add_argument( "-l", "--limit", dest="limit", required=False, - type=int, + type=float, default=5, - help="stop showing duplicates with total size smaller then (5 default) GiB", + help="stop showing duplicates with total size smaller than (5 default) GiB. Set 0 for unlimited.", + ) + parser.add_argument( + "-o", + "--outfile", + dest="outfile", + required=False, + type=str, + help="output tab-delimited file (default STDOUT)", ) parser.add_argument("-v", "--version", action="version", version=__version__) print_with_timestamp( @@ -70,18 +78,27 @@ def main(): for l in filesgz: dfu = fgz() properly_set = dfu.set(l) - if not properly_set: + if not properly_set: # could not read line properly or there are no duplicates continue + if dfu.ndup == 0: continue # in case mimeo was run without -z dups.append(dfu) - dups.sort() + dups.sort() # look at __lt__ ... its sorting from highest to lowest totalsize saved = 0 top_limit = args.limit * 1024 * 1024 * 1024 # 5 GiB + if args.outfile: + of = open(args.outfile, "w") + else: + of = sys.stdout + for fgitem in dups: - saved += fgitem.totalsize if fgitem.totalsize < top_limit: break - print(fgitem) + saved += fgitem.totalsize + of.write("%s\n"%(fgitem)) + + if args.outfile: + of.close() saved = get_human_readable_size(saved) print_with_timestamp( From 7b32b7fd2cf1f8d371d1cae367c3240ae400ccbe Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:05:24 -0500 Subject: [PATCH 03/22] chore: improving spacesavers2_catalog cli help --- spacesavers2_catalog | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spacesavers2_catalog b/spacesavers2_catalog index 5c98749..0ba81ca 100755 --- a/spacesavers2_catalog +++ b/spacesavers2_catalog @@ -63,7 +63,7 @@ def main(): required=False, type=int, default=4, - help="number of threads to be used", + help="number of threads to be used (default 4)", ) parser.add_argument( "-b", @@ -72,7 +72,7 @@ def main(): required=False, type=int, default=128 * 1024, - help="buffersize for xhash creation", + help="buffersize for xhash creation (default=128 * 1028 bytes)", ) parser.add_argument( "-i", @@ -81,7 +81,7 @@ def main(): required=False, type=int, default=1024 * 1024 * 1024, - help="this sized header of the file is ignored before extracting buffer of buffersize for xhash creation (only for special extensions files)", + help="this sized header of the file is ignored before extracting buffer of buffersize for xhash creation (only for special extensions files) default = 1024 * 1024 * 1024 bytes", ) parser.add_argument( "-s", @@ -90,7 +90,7 @@ def main(): required=False, type=str, default="bam,bai,bigwig,bw,csi", - help="comma separated list of special extensions", + help="comma separated list of special extensions (default=bam,bai,bigwig,bw,csi)", ) parser.add_argument( "-o", From f2e7cccc32db9cc701eba44caf745fa19f794f75 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:06:21 -0500 Subject: [PATCH 04/22] fix: logic update, new arguments fix#71 --- spacesavers2_mimeo | 195 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 147 insertions(+), 48 deletions(-) diff --git a/spacesavers2_mimeo b/spacesavers2_mimeo index 9212e41..04a61e3 100755 --- a/spacesavers2_mimeo +++ b/spacesavers2_mimeo @@ -4,6 +4,8 @@ import os import gzip import textwrap import time +import shutil +import subprocess MINDEPTH = 3 QUOTA_TB = 20 @@ -16,11 +18,24 @@ version_check() from src.FileDetails import FileDetails from src.dfUnit import dfUnit from src.Summary import Summary +from src.Summary import pathlen from src.utils import * from datetime import date import argparse +def check_terminal_list(p,tlist): + outcome = -1 # append path to tlist + for i,p2 in enumerate(tlist): + if p.len < p2.len: + if p.path in p2.path: + outcome = -2 # path already in tlist + return outcome + else: + if p2.path in p.path: + outcome = i + return outcome + return outcome def process_hh( uid, @@ -34,51 +49,78 @@ def process_hh( user_output, ): for h in hashhash.keys(): - split_required = False - hashhash[h].compute( - hashhashsplits, split_required + # if files have the same forward and reverse hashes but different sizes then + # hashes are split into multiple hashes with suffix + # being added to the bottom hash for each size + split_required = hashhash[h].compute( + hashhashsplits ) # compute if split is needed or if we have duplicates if split_required: - continue # split is required so move on to the next hash + continue # split is required so move on to the next hash as new hashes with have been created by compute and added to hashhashsplits ... deal with them there! + # get indexes to files in the flist that belong to user with uid + # if uid is zero, then get all file indexes uid_file_index = hashhash[h].get_user_file_index(uid) - if len(uid_file_index) == 0: + if len(uid_file_index) == 0: # user with uid has no files in this set continue + oldest_index = hashhash[h].oldest_index + foldest = hashhash[h].flist[oldest_index] + user_owns_original = False + if foldest.uid == uid or 0 == uid : user_owns_original = True uid_dup_file_index = [] - if hashhash[h].ndup > 1: + if hashhash[h].ndup_inode > 1: # there are duplicate inodes and hence there are duplicate files + inodes_already_summerized = list() for i in uid_file_index: f = hashhash[h].flist[i] fpaths = f.get_paths(mindepth, maxdepth) if ( - i == hashhash[h].oldest_index + i == oldest_index ): # its the original file ... not a duplicate for p in fpaths: + peruser_perfolder_summaries[p].nnondup_files += 1 peruser_perfolder_summaries[p].non_dup_Bytes.append(f.size) peruser_perfolder_summaries[p].non_dup_ages.append(f.mtime) + inodes_already_summerized.append(f.inode) # scenario where original already has a hard-link else: uid_dup_file_index.append(i) - for p in fpaths: - peruser_perfolder_summaries[p].dup_Bytes.append(f.size) - peruser_perfolder_summaries[p].dup_ages.append(f.mtime) - else: # ndup == 1 .. meaning there are no duplicates .. just one file - for i in uid_file_index: - f = hashhash[h].flist[i] - fpaths = f.get_paths(mindepth, maxdepth) - for p in fpaths: - peruser_perfolder_summaries[p].non_dup_Bytes.append(f.size) - peruser_perfolder_summaries[p].non_dup_ages.append(f.mtime) + # has the inode already been summarized + if f.inode in inodes_already_summerized: + for p in fpaths: + peruser_perfolder_summaries[p].ndup_files += 1 + else: + inodes_already_summerized.append(f.inode) + for p in fpaths: + peruser_perfolder_summaries[p].ndup_files+=1 + peruser_perfolder_summaries[p].dup_Bytes.append(f.size) + peruser_perfolder_summaries[p].dup_ages.append(f.mtime) + else: + # ndup_inode == 1 .. meaning there are no duplicate inodes .. can still have multiple hard linked files + # only count 1 file/hardlink for summary + i = uid_file_index[0] + f = hashhash[h].flist[i] + fpaths = f.get_paths(mindepth, maxdepth) + for p in fpaths: + peruser_perfolder_summaries[p].nnondup_files += 1 + peruser_perfolder_summaries[p].non_dup_Bytes.append(f.size) + peruser_perfolder_summaries[p].non_dup_ages.append(f.mtime) if args.duplicatesonly: - if len(uid_dup_file_index) > 0: + if len(uid_dup_file_index) > 0: # this user has some duplicates + out_index = [oldest_index] + out_index.extend(uid_dup_file_index) user_output.write( "{}\n".format( hashhash[h].str_with_name( - uid2uname, gid2gname, uid_dup_file_index + uid2uname, gid2gname, out_index ) ) ) else: + out_index = [] + if user_owns_original == False: + out_index.append(oldest_index) + out_index.extend(uid_file_index) user_output.write( "{}\n".format( - hashhash[h].str_with_name(uid2uname, gid2gname, uid_file_index) + hashhash[h].str_with_name(uid2uname, gid2gname, out_index) ) ) @@ -105,7 +147,7 @@ def main(): parser.add_argument( "-f", "--catalog", - dest="lsout", + dest="catalog", required=True, type=str, default=sys.stdin, @@ -119,7 +161,7 @@ def main(): required=False, type=int, default=10, - help="folder max. depth upto which reports are aggregated", + help="folder max. depth upto which reports are aggregated ... absolute path is used to calculate depth (Default: 10)", ) parser.add_argument( @@ -160,6 +202,16 @@ def main(): action=argparse.BooleanOptionalAction, help="Print only duplicates to per user output file.", ) + + parser.add_argument( + "-k", + "--kronaplot", + dest="kronaplot", + required=False, + action=argparse.BooleanOptionalAction, + help="Make kronaplots for duplicates.(ktImportText must be in PATH!)", + ) + parser.add_argument("-v", "--version", action="version", version=__version__) print_with_timestamp( @@ -170,20 +222,27 @@ def main(): args = parser.parse_args() quota = args.quota * 1024 * 1024 * 1024 * 1024 + if args.kronaplot: + ktImportText_in_path = False + if shutil.which("ktImportText") == None: + sys.stderr.write("ktImportText(from kronaTools) not found in PATH. kronaplots will not be generated.\n") + else: + ktImportText_in_path = True + uid2uname = dict() gid2gname = dict() hashhash = dict() - users = set() # list of all users found + users = set() # list of all uids found users.add(0) # 0 == all users - groups = set() # list of groups - paths = set() - path_lens = [] + groups = set() # list of gids + paths = set() # set of all paths possible + path_lens = set() # set of all path depths print_with_timestamp( start=start, scriptname=scriptname, string="Reading in catalog file..." ) set_complete = True - with open(args.lsout) as lsout: - for l in lsout: + with open(args.catalog) as catalog: + for l in catalog: fd = FileDetails() set_complete = fd.set(l) if not set_complete: @@ -192,7 +251,7 @@ def main(): continue # ignore all symlinks users.add(fd.uid) groups.add(fd.gid) - path_lens.append(get_file_depth(fd.apath)) + path_lens.add(get_file_depth(fd.apath)) for p in fd.get_paths_at_all_depths(): paths.add(p) hash = fd.xhash_top + "#" + fd.xhash_bottom @@ -258,11 +317,19 @@ def main(): outfilenameprefix = get_username_groupname(uid) summaryfilepath = os.path.join( - os.path.abspath(args.outdir), outfilenameprefix + ".summary.txt" + os.path.abspath(args.outdir), outfilenameprefix + ".mimeo.summary.txt" ) useroutputpath = os.path.join( - os.path.abspath(args.outdir), outfilenameprefix + ".files.gz" + os.path.abspath(args.outdir), outfilenameprefix + ".mimeo.files.gz" ) + if args.kronaplot: + kronatsv = os.path.join( + os.path.abspath(args.outdir), outfilenameprefix + ".mimeo.krona.tsv" + ) + if ktImportText_in_path: + kronahtml = os.path.join( + os.path.abspath(args.outdir), outfilenameprefix + ".mimeo.krona.html" + ) with open(summaryfilepath, "w") as user_summary: user_summary.write("%s\n" % (Summary.HEADER)) @@ -273,9 +340,7 @@ def main(): peruser_perfolder_summaries = dict() for p in paths: peruser_perfolder_summaries[p] = Summary(p) - hashhashsplits = ( - dict() - ) # dict to collect instances where the files are NOT duplicates has same hashes but different sizes (and different inodes) ... new suffix is added to bottomhash .."_iterator" + hashhashsplits = dict() # dict to collect instances where the files are NOT duplicates has same hashes but different sizes (and different inodes) ... new suffix is added to bottomhash .."_iterator" process_hh( uid, hashhash, @@ -287,25 +352,59 @@ def main(): peruser_perfolder_summaries, user_output, ) - hashhashsplitsdummy = dict() - process_hh( - uid, - hashhashsplits, - hashhashsplitsdummy, - mindepth, - maxdepth, - uid2uname, - gid2gname, - peruser_perfolder_summaries, - user_output, - ) + if len(hashhashsplits) != 0: + hashhashsplitsdummy = dict() + process_hh( + uid, + hashhashsplits, + hashhashsplitsdummy, + mindepth, + maxdepth, + uid2uname, + gid2gname, + peruser_perfolder_summaries, + user_output, + ) + del hashhashsplitsdummy + del hashhashsplits for p in paths: peruser_perfolder_summaries[p].update_scores(quota) user_summary.write(f"{peruser_perfolder_summaries[p]}\n") + + if args.kronaplot: + terminal_paths = [] + with open(summaryfilepath,'r') as infile: + count = 0 + for l in infile: + l = l.strip().split("\t") + count += 1 + if count==1: + continue #header + if count==2: + terminal_paths.append(pathlen(l[0],int(l[2]))) + continue + p = pathlen(l[0],int(l[2])) + outcome = check_terminal_list(p,terminal_paths) + if outcome == -1: # new ... append + terminal_paths.append(p) + elif outcome == -2: # already in list .. move on + continue + elif outcome > -1: # better than current one in list ... swap + terminal_paths[outcome] = p + with open(kronatsv,'w') as ktsv: + for p in terminal_paths: + path = p.path + path = path.replace('/','\t') + path = path.replace('\t\t','\t') + if p.dupbytes != 0 : + ktsv.write("%d\t%s\n"%(p.dupbytes,path)) + if ktImportText_in_path: + cmd = "ktImportText %s -o %s"%(kronatsv,kronahtml) + srun = subprocess.run(cmd,shell=True, capture_output=True, text=True) + if srun.returncode !=0: + sys.stderr.write("%s\n"%(srun.stderr)) del hashhash - del hashhashsplits - del hashhashsplitsdummy print_with_timestamp(start=start, scriptname=scriptname, string="Finished!") From d0c5c52da248445809ecf1859dd003335c73a0a7 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:07:08 -0500 Subject: [PATCH 05/22] fix: changes from mimeo trickling down --- spacesavers2_usurp | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/spacesavers2_usurp b/spacesavers2_usurp index baeded3..1ed5cac 100755 --- a/spacesavers2_usurp +++ b/spacesavers2_usurp @@ -70,9 +70,6 @@ def main(): global args args = parser.parse_args() - # if args.version: - # version_print() - print_with_timestamp( start=start, scriptname=scriptname, string="version: {}".format(__version__) ) @@ -83,18 +80,30 @@ def main(): l = l.strip().split("\t") lhash = l[0] if args.hash in lhash: - dupfiles = l[4].split(";") - original_copy = Path(dupfiles.pop(0).strip('"')) + original_copy = Path(l[4].strip('"')) + dupfiles = l[5].split(";") print_with_timestamp( start=start, scriptname=scriptname, string="Original copy: {}".format(original_copy), ) + if not os.access(original_copy, os.R_OK): + print_with_timestamp( + start=start, + scriptname=scriptname, + string="Original copy is not readable. Hardlinks cannot be created!", + ) + exit(1) + inode_set = set() for dup in dupfiles: dup = Path(dup.strip('"')) duptmp = Path(str(dup) + "." + str(uuid.uuid4())) st = os.stat(dup) - total_saved += st.st_size + fsize = st.st_size + finode = st.st_ino + if not finode in inode_set: + inode_set.add(finode) + total_saved += fsize print_with_timestamp( start=start, scriptname=scriptname, @@ -110,20 +119,21 @@ def main(): os.remove(dup) os.rename(duptmp, dup) except OSError: - print_with_timestamp( - start=start, - scriptname=scriptname, - string="OSError occurred while creating hard-link. Probably trying to create a cross-device hard-link", - ) if args.force: print_with_timestamp( start=start, scriptname=scriptname, - string="Creating sym-link instead!", + string="Creating symlink file: {}".format(dup), ) os.remove(dup) os.symlink(original_copy, dup) - break + else: + print_with_timestamp( + start=start, + scriptname=scriptname, + string="OSError occurred while creating hard-link. Probably trying to create a cross-device hard-link. Try using --force to create symlink instead.", + ) + # break total_saved_human_readable = get_human_readable_size(total_saved) print_with_timestamp( start=start, From 57ce2495494c66c9893069b45205a6d6f01805ef Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:07:44 -0500 Subject: [PATCH 06/22] chore: finddup replaced with mimeo etc. --- src/FileDetails.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/FileDetails.py b/src/FileDetails.py index 90d786d..1204257 100644 --- a/src/FileDetails.py +++ b/src/FileDetails.py @@ -115,7 +115,7 @@ def set(self,ls_line): # exit() return False - def str_with_name(self,uid2uname,gid2gname):# method for printing output in finddup ... replace "xhash_top;xhash_bottom" with "username;groupname" at the end of the string + def str_with_name(self,uid2uname,gid2gname):# method for printing output in mimeo ... replace "xhash_top;xhash_bottom" with "username;groupname" at the end of the string # return_str = "\"%s\";"%(self.apath) # path may have newline char which should not be interpretted as new line char return_str = "\"%s\";"%(str(self.apath).encode('unicode_escape').decode('utf-8')) @@ -139,7 +139,7 @@ def __str__(self): return_str = "\"%s\";"%(str(self.apath).encode('unicode_escape').decode('utf-8')) return_str += "%s;"%(self.issyml) return_str += "%d;"%(self.size) - return_str += "%d;"%(self.dev) + return_str += "%d;"%(self.dev) # device id return_str += "%d;"%(self.inode) return_str += "%d;"%(self.nlink) return_str += "%d;"%(self.atime) @@ -152,10 +152,11 @@ def __str__(self): return return_str def get_paths_at_all_depths(self): # for files - return self.apath.parents[:-1] + return self.apath.parents[:-1] # remove the last one ... which will be '/' def get_paths(self,mindepth,maxdepth): parents = list(self.apath.parents[0:-1]) parents = list(filter(lambda x:get_folder_depth(x) <= maxdepth,parents)) parents = list(filter(lambda x:get_folder_depth(x) >= mindepth,parents)) return parents + From c404e9683c7a98b2066495884e7e426f4915e380 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:13:41 -0500 Subject: [PATCH 07/22] fix: updates for mimeo, blamematrix improvements --- src/Summary.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Summary.py b/src/Summary.py index e055f0a..fe5ab54 100644 --- a/src/Summary.py +++ b/src/Summary.py @@ -29,6 +29,8 @@ class Summary: def __init__(self,path): self.path = path + self.nnondup_files = 0 + self.ndup_files = 0 self.non_dup_Bytes = [] self.dup_Bytes = [] self.non_dup_ages = [] @@ -78,8 +80,10 @@ def __str__(self): tot_mean_age = (sum(self.dup_ages) + sum(self.non_dup_ages))/(len(self.dup_ages)+len(self.non_dup_ages)) except ZeroDivisionError: tot_mean_age = 0 - dup_files = len(self.dup_Bytes) - tot_files = dup_files + len(self.non_dup_Bytes) + # dup_files = len(self.dup_Bytes) + # tot_files = dup_files + len(self.non_dup_Bytes) + dup_files = self.ndup_files + tot_files = self.nnondup_files + dup_files return_str = str(self.path)+"\t" return_str += "%d\t"%(tot_Bytes) return_str += "%d\t"%(dup_Bytes) @@ -101,3 +105,12 @@ def __str__(self): return_str += "%d"%(self.OverallScore) return return_str +class pathlen: + def __init__(self,p,dupbytes): + self.path=p + self.len=len(p.split("/")) + self.dupbytes=dupbytes + + def __str__(self): + returnstr="%s"%(self.path) + return returnstr From c14950518dd772a3892776364a33c751889fe782 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:14:25 -0500 Subject: [PATCH 08/22] fix: improvements, new logic required for fixing #71 --- src/dfUnit.py | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/dfUnit.py b/src/dfUnit.py index 6100e1f..6be1a68 100644 --- a/src/dfUnit.py +++ b/src/dfUnit.py @@ -15,15 +15,19 @@ def __init__(self,hash): self.flist = [] # list of _ls files with the same hash self.fsize = -1 # size of each file self.ndup = -1 # files in flist with same size, but different inode (they already have the same hash) + self.ndup_files = -1 # number of duplicate files ... used for counting duplicate files + self.ndup_inode = -1 # number of duplicate inodes ... used for counting duplicate bytes self.size_set = set() # set of unique sizes ... if len(size_set) then split is required self.uid_list = [] # list of uids of files added self.inode_list = [] # list of inodes of files added - self.oldest_inode = -1 # oldest_ ... is for the file which is NOT the duplicate + self.oldest_inode = -1 # oldest_ ... is for the file which is NOT the duplicate or is the original self.oldest_index = -1 self.oldest_age = -1 self.oldest_uid = -1 - + def nfiles_with_hash(self): # return number of files in this hash (total ... all users included) + return len(self.flist) + def add_fd(self,fd): # add the file to flist self.flist.append(fd) @@ -44,7 +48,14 @@ def filter_flist_by_uid(self,uid): for i,f in enumerate(self.flist): if f.uid == uid : self.keep.append(i) - def compute(self,hashhashsplits,split_required): # 1. move oldest to the first position 2. find ndup 3. find size 4. filter by uid 5. get depth folder + def compute(self,hashhashsplits): + # find if files have the same hashes, but different sizes then... + # 1. split them into different hashes by size and + # 2. append them to hashhashsplits + # else ... aka .. .no spliting is required + # 1. count number of duplicate inodes and + # 2. size of each file + split_required = False # check if spliting is required if len(self.size_set) > 1: # more than 1 size in this hash split_required = True @@ -57,13 +68,16 @@ def compute(self,hashhashsplits,split_required): # 1. move oldest to the first p if fd.size == size: hashhashsplits[newhash].add_fd(fd) else: # there only 1 size ... no splits required - self.ndup = len(self.inode_list) - 1 #ndup is zero if same size and only 1 inode + self.ndup = len(self.inode_list) - 1 #ndup is zero if same len(size_set)==1 and len(inode_list)==1 + self.ndup_inode = len(set(self.inode_list)) - 1 + self.ndup_files = len(self.inode_list) - 1 self.fsize = self.flist[0].size + return split_required def get_user_file_index(self,uid): uid_file_index = [] if not uid in self.uid_list: - if uid == 0: uid_file_index = list(range(0,len(self.flist))) + if uid == 0: uid_file_index = list(range(0,len(self.flist))) # uid == 0 is all users return uid_file_index else: for i,j in enumerate(self.flist): @@ -82,10 +96,11 @@ def str_with_name(self,uid2uname, gid2gname,findex): class fgz: # used by grubber def __init__(self): self.hash = "" - self.ndup = -1 + self.ndup = -1 # number of duplicate files and not duplicate inodes self.filesize = -1 self.totalsize = -1 - self.fds = [] + self.fds = [] # list of duplicate files + self.of = "" # original file def __lt__(self,other): return self.totalsize > other.totalsize @@ -96,6 +111,7 @@ def __str__(self): outstring.append(str(self.ndup)) outstring.append(get_human_readable_size(self.totalsize)) outstring.append(get_human_readable_size(self.filesize)) + outstring.append(get_filename_from_fgzlistitem(self.of)) outstring.append(";".join(map(lambda x:get_filename_from_fgzlistitem(x),self.fds))) return "\t".join(outstring) # return "{0} {1} {2} {3} {4}".format(self.hash,self.ndup,get_human_readable_size(self.totalsize), get_human_readable_size(self.filesize), ";".join(map(lambda x:get_filename_from_fgzlistitem(x),self.fds))) @@ -107,20 +123,22 @@ def set(self,inputline): try: inputline = inputline.strip().split(" ") if len(inputline) < 5: - raise Exception("Less than 5 items in the line.") + raise Exception("Less than 5 items in mimeo.files.gz line.") self.hash = inputline.pop(0) - dummy = inputline.pop(0) + dummy = inputline.pop(0) # the colon total_ndup = int(inputline.pop(0)) - if total_ndup == 0: # may be finddup was run to output all files .. not just dups + if total_ndup == 0: # may be mimeo was run to output all files .. not just dups .. aka without the -z option return False self.filesize = int(inputline.pop(0)) - full_fds = " ".join(inputline) + full_fds = " ".join(inputline) # bcos file names can contain spaces fds = full_fds.split("##") - self.ndup = len(fds) # these are user number of duplicates/files - if self.ndup == (total_ndup + 1): # one file is the original ... other are all duplicates - dummy = fds.pop(0) - self.ndup -= 1 - self.fds = fds + self.ndup = total_ndup # these are user number of duplicates/files + self.of = fds.pop(0) # one file is the original + self.fds = fds # others are dupicates + inodes_set = set() + for f in fds: + l = f.split(";") + inodes_set.add(l[-7]) self.totalsize = self.ndup * self.filesize return True except: From a8b79055d65827343f7324ff025f77d50f77dab4 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:14:55 -0500 Subject: [PATCH 09/22] chore: help improvement --- src/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils.py b/src/utils.py index 0c67e74..279046d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -73,6 +73,10 @@ def get_folder_depth(path): def get_file_depth(path): +# example +# >>> len(list(Path("/f1/f2/f3/f4/a.xyz").absolute().parents))-1 +# 4 +# a.k.a. file a.xyz is 4 folders deep try: return len(list(path.parents)) - 1 except: From ef2ba911bd5da2aeda28641dc3d4960fb65bc818 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:15:33 -0500 Subject: [PATCH 10/22] feat: e2e complete overhaul --- spacesavers2_e2e | 68 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/spacesavers2_e2e b/spacesavers2_e2e index 5fe2a3e..26a3744 100755 --- a/spacesavers2_e2e +++ b/spacesavers2_e2e @@ -11,10 +11,12 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ARGPARSE_DESCRIPTION="End-to-end run of spacesavers2" source ${SCRIPT_DIR}/resources/argparse.bash || exit 1 argparse "$@" < ${OUTFOLDER}/${outfile_catalog} 2> ${OUTFOLDER}/${outfile_catalog_err} +spacesavers2_catalog \ + --folder $FOLDER \ + --threads $THREADS \ + --outfile ${outfile_catalog} \ + --bottomhash \ + > ${outfile_catalog_log} 2> ${outfile_catalog_err} fi + +# spacesavers2_mimeo if [ "$?" == "0" ];then -echo "Running spacesavers2_mimeo" && \ -spacesavers2_mimeo -f ${OUTFOLDER}/${outfile_catalog} -o ${OUTFOLDER} -q $QUOTA -z -d 3 -p $prefix > ${OUTFOLDER}/${outfile_mimeo_log} 2> ${OUTFOLDER}/${outfile_mimeo_err} +echo "Running spacesavers2_mimeo" +command -V ktImportText 2>/dev/null || module load kronatools || (>&2 echo "module kronatools could not be loaded"; exit 1) +spacesavers2_mimeo \ + --catalog ${outfile_catalog} \ + --outdir ${OUTFOLDER} \ + --quota $QUOTA \ + --duplicatesonly \ + --maxdepth 3 \ + --p $prefix \ + --kronaplot \ + > ${outfile_mimeo_log} 2> ${outfile_mimeo_err} fi + +# spacesavers2_grubbers if [ "$?" == "0" ];then echo "Running spacesavers2_grubbers" && \ -for f in `ls ${OUTFOLDER}/${prefix}*files.gz`;do - outfile=`echo $f|sed "s/files.gz/grubbers.tsv/g"` - errfile=`echo $f|sed "s/files.gz/grubbers.err/g"` - spacesavers2_grubbers -f $f > $outfile 2> $errfile +for filegz in `ls ${OUTFOLDER}/${prefix}*files.gz`;do + outfile=`echo $filegz|sed "s/mimeo.files.gz/grubbers.tsv/g"` + logfile=`echo $filegz|sed "s/mimeo.files.gz/grubbers.log/g"` + errfile=`echo $filegz|sed "s/mimeo.files.gz/grubbers.err/g"` + spacesavers2_grubbers \ + --filesgz $filegz \ + --limit $LIMIT \ + --outfile $outfile \ + > $logfile 2> $errfile done fi + +# spacesavers2_blamematrix if [ "$?" == "0" ];then echo "Running spacesavers2_blamematrix" && \ -spacesavers2_blamematrix -f ${OUTFOLDER}/${prefix}.allusers.files.gz > ${OUTFOLDER}/${prefix}.blamematrix.tsv 2> ${OUTFOLDER}/${prefix}.blamematrix.err +spacesavers2_blamematrix \ + --filesgz ${OUTFOLDER}/${prefix}.allusers.mimeo.files.gz \ + --level $LEVEL \ + --outfile ${outfile_blamematrix} \ + > ${outfile_blamematrix_log} 2> ${outfile_blamematrix_err} fi echo "Done!" From e5286f75f6a3b8b519acc758e85c2da0bb6589ff Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:16:39 -0500 Subject: [PATCH 11/22] docs: updated docs to reflect new changes --- docs/blamematrix.md | 27 +++++++++++++++++++-------- docs/catalog.md | 2 +- docs/grubbers.md | 37 +++++++++++++++++++------------------ docs/mimeo.md | 36 ++++++++++++++++++++++-------------- docs/usurp.md | 8 ++++---- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/docs/blamematrix.md b/docs/blamematrix.md index e4f60f6..7868c96 100644 --- a/docs/blamematrix.md +++ b/docs/blamematrix.md @@ -1,6 +1,6 @@ -## spacesavers2_grubbers +## spacesavers2_blamematrix -This takes in the `allusers.files.gz` generated by `spacesavers2_mimeo` and processes it to create a matrix with: +This takes in the `allusers.mimeo.files.gz` generated by `spacesavers2_mimeo` and processes it to create a matrix with: - folder paths as row-names - usernames as column-names @@ -11,28 +11,39 @@ Deleting these high-value duplicates first will have the biggest impact on the u ### Inputs - `--filesgz` output file from `spacesavers2_mimeo`. -- `--limit` lower cut-off for output display (default 5 GiB). This means that duplicates with overall size of less than 5 GiB will not be displayed. +- `--level` depth at with to cutoff the output. +- `--humanreable` make the output human readable, that is, output in MiB, GiB, TiB, etc. instead of bytes. +- `--includezeros` include empty folders. +- `--outfile` path to the output file. ```bash % spacesavers2_blamematrix --help -spacesavers2_blamematrix:00000.19s:version: v0.5 -usage: spacesavers2_blamematrix [-h] -f FILESGZ [-l LEVEL] +usage: spacesavers2_blamematrix [-h] -f FILESGZ [-l LEVEL] [-r | --humanreable | --no-humanreable] [-z | --includezeros | --no-includezeros] [-o OUTFILE] [-v] spacesavers2_blamematrix: get per user duplicate sizes at a given folder level (default 3) options: -h, --help show this help message and exit -f FILESGZ, --filesgz FILESGZ - spacesavers2_mimeo prefix.allusers.files.gz file + spacesavers2_mimeo prefix.allusers.mimeo.files.gz file -l LEVEL, --level LEVEL folder level to use for creating matrix + -r, --humanreable, --no-humanreable + sizes are printed in human readable format ... (default: Bytes) + -z, --includezeros, --no-includezeros + include folders where totalbytes is zero. + -o OUTFILE, --outfile OUTFILE + output tab-delimited file (default STDOUT) + -v, --version show program's version number and exit Version: - v0.5 + v0.10.2-dev Example: - > spacesavers2_blamematrix -f /output/from/spacesavers2_mimeo/prefix.allusers.files.gz -d 3 + > spacesavers2_blamematrix -f /output/from/spacesavers2_mimeo/prefix.allusers.mimeo.files.gz -d 3 -o prefix.blamematrix.tsv ``` ### Outputs Counts matrix with duplicate bytes per user per folder. + +> Note this can be used to generate a heatmap for quickly find folders with high duplicates and the user they belong to. diff --git a/docs/catalog.md b/docs/catalog.md index 6405d9b..5b51044 100644 --- a/docs/catalog.md +++ b/docs/catalog.md @@ -58,7 +58,7 @@ Example: `spacesavers2_catalog` creates one semi-colon seperated output line per input file. Here is an example line: ```bash -% head -n1 test.ls_out +% head -n1 test.catalog "/data/CBLCCBR/kopardevn_tmp/spacesavers2_testing/_data_CCBR_Pipeliner_db_PipeDB_Indices.ls.old";False;1653453;47;372851499;1;1;5;5;37513;57886;4707e661a1f3beca1861b9e0e0177461;52e5038016c3dce5b6cdab635765cc79; ``` The 13 items in the line are as follows: diff --git a/docs/grubbers.md b/docs/grubbers.md index 5c38682..a71e9e2 100644 --- a/docs/grubbers.md +++ b/docs/grubbers.md @@ -1,35 +1,38 @@ ## spacesavers2_grubbers -This takes in the `.files.gz` generated by `spacesavers2_mimeo` and processes it to: +This takes in the `mimeo.files.gz` generated by `spacesavers2_mimeo` and processes it to: - sort duplicates by total size - reports the "high-value" duplicates. -Deleting these high-value duplicates first will have the biggest impact on the users overall digital footprint +Deleting these high-value duplicates first will have the biggest impact on the users overall digital footprint. ### Inputs - `--filesgz` output file from `spacesavers2_mimeo`. -- `--limit` lower cut-off for output display (default 5 GiB). This means that duplicates with overall size of less than 5 GiB will not be displayed. +- `--limit` lower cut-off for output display (default 5 GiB). This means that duplicates with overall size of less than 5 GiB will not be displayed. Set 0 to report all. ```bash -% spacesavers2_grubbers --help -spacesavers2_grubbers:00000.01s:version: v0.5 -usage: spacesavers2_grubbers [-h] -f FILESGZ [-l LIMIT] +╰─○ spacesavers2_grubbers --help +spacesavers2_grubbers:00000.00s:version: v0.10.2-dev +usage: spacesavers2_grubbers [-h] -f FILESGZ [-l LIMIT] [-o OUTFILE] [-v] spacesavers2_grubbers: get list of large duplicates sorted by total size options: -h, --help show this help message and exit -f FILESGZ, --filesgz FILESGZ - spacesavers2_mimeo prefix..files.gz file + spacesavers2_mimeo prefix..mimeo.files.gz file -l LIMIT, --limit LIMIT - stop showing duplicates with total size smaller then (5 default) GiB + stop showing duplicates with total size smaller than (5 default) GiB. Set 0 for unlimited. + -o OUTFILE, --outfile OUTFILE + output tab-delimited file (default STDOUT) + -v, --version show program's version number and exit Version: - v0.5 + v0.10.2-dev Example: - > spacesavers2_grubbers -f /output/from/spacesavers2_mimeo/prefix.files.gz + > spacesavers2_grubbers -f /output/from/spacesavers2_finddup/prefix.files.gz ``` ### Outputs @@ -40,18 +43,16 @@ The output is displayed on STDOUT and is tab-delimited with these columns: | ------ | ------------------------------------- | | 1 | combined hash | | 2 | number of duplicates found | -| 3 | total size of all duplicates | -| 4 | size of each duplicate | -| 5 | ";"-separated list of duplicates | -| 6 | duplicate files | +| 3 | total size of all duplicates (human readable) | +| 4 | size of each duplicate (human readable) | +| 5 | original file | +| 6 | ";"-separated list of duplicates files | Here is an example output line: ```bash -ca269c980de3f0d8e6668b88d9065c8f#5003f92f52d71437741e4e79c4339a66 3 21.99 GiB 7.33 GiB "/data/CCBR/ccbr754_Yoshimi/ccbr754/workdir_170403_postinitialrnas -eq2/0h_1_S25.p2.Aligned.toTranscriptome.sorted.bam";"/data/CCBR/ccbr754_Yoshimi/ccbr754targz/data/CCBR/projects/ccbr754/workdir_170403_postinitialrnaseq2/0h_1_S25.p2.Aligned.toTr -anscriptome.sorted.bam";"/data/CCBR/ccbr754_Yoshimi/ccbr754targz/data/CCBR/projects/ccbr754/workdir_170403_postinitialrnaseq2/0h_1_S25.p2.Aligned.toTranscriptome.sorted.sorted.ba -m" +183e9dc341073d9b75c817f5ed07b9ac#183e9dc341073d9b75c817f5ed07b9ac 5 0.07 KiB 0.01 KiB "/data/CCBR/abdelmaksoudaa/test/a" "/data/CCBR/abdelmaksoudaa/test/b";"/data/CCBR/abde +lmaksoudaa/test/c";"/data/CCBR/abdelmaksoudaa/test/d";"/data/CCBR/abdelmaksoudaa/test/e";"/data/CCBR/abdelmaksoudaa/test/f" ``` > `spacesavers2_grubbers` is typical used to find the "low-hanging" fruits ... aka ... the "high-value" duplicates which need to be deleted first to quickly have the biggest impact on the users overall digital footprint. \ No newline at end of file diff --git a/docs/mimeo.md b/docs/mimeo.md index 215ae48..d3d598f 100644 --- a/docs/mimeo.md +++ b/docs/mimeo.md @@ -1,13 +1,13 @@ ## spacesavers2_mimeo -This takes in the `ls_out` generated by `spacesavers2_catalog` and processes it to: +This takes in the `catalog` file generated by `spacesavers2_catalog` and processes it to: - find duplicates - create per-user summary reports for each user (and all users). ### Inputs -- `--lsout` is the output file from `spacesavers2_catalog`. Thus, `spacesavers2_catalog` needs to be run before running `spacesavers2_mimeo`. +- `--catalog` is the output file from `spacesavers2_catalog`. Thus, `spacesavers2_catalog` needs to be run before running `spacesavers2_mimeo`. - `--maxdepth` maximum folder depth upto which reports are aggregated - `--outdir` path to the output folder - `--prefix` prefix to be added to the output file names eg. date etc. @@ -16,17 +16,16 @@ This takes in the `ls_out` generated by `spacesavers2_catalog` and processes it ```bash % spacesavers2_mimeo --help -spacesavers2_mimeo:00000.02s:version: v0.5 -usage: spacesavers2_mimeo [-h] -f LSOUT [-d MAXDEPTH] [-o OUTDIR] [-p PREFIX] [-q QUOTA] [-z | --duplicatesonly | --no-duplicatesonly] +usage: spacesavers2_mimeo [-h] -f CATALOG [-d MAXDEPTH] [-o OUTDIR] [-p PREFIX] [-q QUOTA] [-z | --duplicatesonly | --no-duplicatesonly] [-k | --kronaplot | --no-kronaplot] [-v] spacesavers2_mimeo: find duplicates options: -h, --help show this help message and exit - -f LSOUT, --catalog LSOUT + -f CATALOG, --catalog CATALOG spacesavers2_catalog output from STDIN or from catalog file -d MAXDEPTH, --maxdepth MAXDEPTH - folder max. depth upto which reports are aggregated + folder max. depth upto which reports are aggregated ... absolute path is used to calculate depth (Default: 10) -o OUTDIR, --outdir OUTDIR output folder -p PREFIX, --prefix PREFIX @@ -35,16 +34,19 @@ options: total quota of the mount eg. 200 TB for /data/CCBR -z, --duplicatesonly, --no-duplicatesonly Print only duplicates to per user output file. + -k, --kronaplot, --no-kronaplot + Make kronaplots for duplicates.(ktImportText must be in PATH!) + -v, --version show program's version number and exit Version: - v0.5 + v0.10.2-dev Example: - > spacesavers2_mimeo -f /output/from/spacesavers2_catalog -o /path/to/output/folder -d 7 -q 10 + > spacesavers2_mimeo -f /output/from/spacesavers2_catalog -o /path/to/output/folder -d 7 -q 10 -k ``` ### Outputs -After completion of run, `spacesavers2_mimeo` creates `.files.gz` (list of duplicate files) and `.summary.txt` (overall stats at various depths) files in the provided output folder. Here are the details: +After completion of run, `spacesavers2_mimeo` creates `*.mimeo.files.gz` (list of files per user + one "allusers" file) and `.summary.txt` (overall stats at various depths) files in the provided output folder. if `-k` is provided (and ktImportText from [kronatools](https://github.com/marbl/Krona/wiki/KronaTools) is in PATH) then krona specific TSV and HTML pages are also generated. Here are the details: #### Duplicates @@ -54,7 +56,7 @@ After completion of run, `spacesavers2_mimeo` creates `.files.gz` (list of dupli - Check if each bin has unique sized files. If a bin has more than 1 size, then it needs to be binned further. Sometimes, xxHash of top and bottom chunks also gives the same combination of hash for differing files. These files will have different sizes. Hence, re-bin them accordingly. - If same size, then check inodes. If all files in the same bin have the same inode, then these are just hard-links. But, if there are multiple inodes, then we have **duplicates**! - If we have duplicates, then `spacesavers2_mimeo` keeps track of number of duplicates per bin. Number of duplicates is equal to number of inodes in each bin minus one. -- If we have duplicates, then the oldest find is identified and considered to be the original file. All other files are marked _duplicate_, irrespective of user id. +- If we have duplicates, then the oldest file is identified and considered to be the original file. All other files are marked _duplicate_, irrespective of user id. - duplicate files are reported in gzip format with the following columns for all users and per-user basis Here is what the `.files.gz` file columns (space-separated) represent: @@ -63,17 +65,19 @@ Here is what the `.files.gz` file columns (space-separated) represent: | ------ | ------------------------------------------------ | | 1 | top chunk and bottom chunk hashes separated by "#" | | 2 | separator ":" | -| 3 | Number of duplicates | +| 3 | Number of duplicates files (not duplicate inodes) | | 4 | Size of each file | | 5 | List of users duplicates serapated by "##" | -Each file in the last column above is ":" separated with the same 13 items as described in the `ls_out` file. The only difference is that the user id and group id are now replaced by user name and group name. +> NOTE: Number of dupicate files can be greater than number of duplicate inodes as each file can have multiple hard links already. Hence, while calculating total duplicate bytes we use (total_number_of_unique_inodes_per_group_of_duplicate_files - 1) X size_of_each_file. The "minus 1" is to not count the size of the original file. -Along with creating one `.files.gz` and `.summary.txt` file per user encountered, `spacesavers2_mimeo` also generates a `allusers.files.gz` file for all users combined. This file is later used by `spacesavers2_blamematrix` as input. +Each file in the last column above is ";" separated with the same 13 items as described in the `catalog` file. The only difference is that the username and groupame are now appended to each file entry. + +Along with creating one `.mimeo.files.gz` and `.mimeo.summary.txt` file per user encountered, `spacesavers2_mimeo` also generates a `allusers.mimeo.files.gz` file for all users combined. This file is later used by `spacesavers2_blamematrix` as input. #### Summaries -Summaries, files ending with `.summary.txt` are collected and reported for all users (`allusers.summary.txt`) and per-user (`USERNAME.summary.txt`) basis for user-defined depth (and beyond). The columns (tab-delimited) in the summary file: +Summaries, files ending with `.mimeo.summary.txt` are collected and reported for all users (`allusers.mimeo.summary.txt`) and per-user (`USERNAME.mimeo.summary.txt`) basis for user-defined depth (and beyond). The columns (tab-delimited) in the summary file: | Column | Description | | ------ | ------------------------------------- | @@ -93,3 +97,7 @@ Summaries, files ending with `.summary.txt` are collected and reported for all u For columns 10 through 13, the same logic is used as [spacesavers](https://ccbr.github.io/spacesavers/usage/df/). +#### KronaTSV and KronaHTML + +- KronaTSV is tab-delimited with first column showing the number of duplicate bytes and every subsequent column giving the folder depths. +- ktImportText is then used to convert the KronaTSV to KronaHTML which can be shared easily and only needs a HTML5 supporting browser for viewing. \ No newline at end of file diff --git a/docs/usurp.md b/docs/usurp.md index 3580055..708c6ad 100644 --- a/docs/usurp.md +++ b/docs/usurp.md @@ -19,10 +19,10 @@ The GRUBBER file has the following columns: | ------ | ------------------------------------- | | 1 | combined hash | | 2 | number of duplicates found | -| 3 | total size of all duplicates | -| 4 | size of each duplicate | -| 5 | ";"-separated list of duplicates | -| 6 | duplicate files | +| 3 | total size of all duplicates (human readable) | +| 4 | size of each duplicate (human readable) | +| 5 | original file | +| 6 | ";"-separated list of duplicates files | ```bash usage: spacesavers2_usurp [-h] -g GRUBBER -x HASH [-f | --force | --no-force] From a08c04e6b02f09c6c66a7206e681466220df511e Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:17:17 -0500 Subject: [PATCH 12/22] docs: changelog updates --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f9e4b..d3b221e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ ### New features - adding `requirements.txt` for easy creation of environment in "spacesavers2" docker (#68, @kopardev) +- `grubbers` `--limit` can be < 1 GiB (float) (#70, @kopardev) +- `grubbers` has new `--outfile` argument. +- `grubbers` output file format changed. New original file column added. +- `blamematrix` has 3 new arguments `--humanreable`, `--includezeros` and `--outfile`. +- `mimeo` `--duplicateonly` logic fix (#71, @kopardev) +- `mimeo` files.gz always includes the original file as the first one in the filelist. +- `mimeo` now has kronatools compatible output. ktImportText is also run if in PATH to generate HTML report for duplicates only. (#46, @kopardev) +- `e2e` overhauled, improved and well commented. +- documentation updated. ## Bug fixes From cf5744c36173c5c88b1db3db531adb39fa5f7711 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:17:56 -0500 Subject: [PATCH 13/22] chore: adding dev prefix to version --- src/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VERSION b/src/VERSION index 5eef0f1..c039361 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -0.10.2 +0.10.2-dev From 6d7a2a2c031a7307234be3c09051da21ecdaaa39 Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:20:53 -0500 Subject: [PATCH 14/22] chore:updating version --- src/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VERSION b/src/VERSION index c039361..d9df1bb 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -0.10.2-dev +0.11.0 From 99b3c49cd84d172e9d305292243e7be50b7bfc9f Mon Sep 17 00:00:00 2001 From: kopardev Date: Mon, 5 Feb 2024 17:26:15 -0500 Subject: [PATCH 15/22] chore: updates to CHANGELOG --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b221e..359a6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,27 @@ ### New features +### Bug fixes + +## spacesavers2 0.11.0 + +### New features + - adding `requirements.txt` for easy creation of environment in "spacesavers2" docker (#68, @kopardev) -- `grubbers` `--limit` can be < 1 GiB (float) (#70, @kopardev) - `grubbers` has new `--outfile` argument. -- `grubbers` output file format changed. New original file column added. - `blamematrix` has 3 new arguments `--humanreable`, `--includezeros` and `--outfile`. -- `mimeo` `--duplicateonly` logic fix (#71, @kopardev) - `mimeo` files.gz always includes the original file as the first one in the filelist. - `mimeo` now has kronatools compatible output. ktImportText is also run if in PATH to generate HTML report for duplicates only. (#46, @kopardev) -- `e2e` overhauled, improved and well commented. - documentation updated. -## Bug fixes +### Bug fixes -- +- `grubbers` `--limit` can be < 1 GiB (float) (#70, @kopardev) +- `grubbers` output file format changed. New original file column added. Original file is required by `usurp` +- `mimeo` `--duplicateonly` logic fix (#71, @kopardev) +- `blamematrix` fixed to account for changes due to #71 +- `usurp` fixed to account for changes due to #71. Now using the new "original file" column while creating hard-links. +- `e2e` overhauled, improved and well commented. ## spacesavers2 0.10.2 From ccef8838debb696bb4e68b8f9fae65d03679ced4 Mon Sep 17 00:00:00 2001 From: kopardev Date: Wed, 7 Feb 2024 13:28:52 -0500 Subject: [PATCH 16/22] updates --- CHANGELOG.md | 4 +- README.md | 8 +- docs/assets/images/spacesavers2.png | Bin 41865 -> 79102 bytes docs/blamematrix.md | 49 ------- mkdocs.yml | 1 - spacesavers2_blamematrix | 144 -------------------- spacesavers2_catalog | 39 ++++-- spacesavers2_e2e | 21 ++- spacesavers2_grubbers | 6 +- spacesavers2_mimeo | 203 ++++++++++++++-------------- spacesavers2_usurp | 6 +- src/FileDetails.py | 126 +++++++++++------ src/Summary.py | 3 +- src/dfUnit.py | 105 ++------------ src/utils.py | 12 -- 15 files changed, 241 insertions(+), 486 deletions(-) delete mode 100644 docs/blamematrix.md delete mode 100755 spacesavers2_blamematrix diff --git a/CHANGELOG.md b/CHANGELOG.md index 359a6c0..c7f264d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - adding `requirements.txt` for easy creation of environment in "spacesavers2" docker (#68, @kopardev) - `grubbers` has new `--outfile` argument. -- `blamematrix` has 3 new arguments `--humanreable`, `--includezeros` and `--outfile`. +- `blamematrix` has now been moved into `mimeo`. - `mimeo` files.gz always includes the original file as the first one in the filelist. - `mimeo` now has kronatools compatible output. ktImportText is also run if in PATH to generate HTML report for duplicates only. (#46, @kopardev) - documentation updated. @@ -23,6 +23,8 @@ - `blamematrix` fixed to account for changes due to #71 - `usurp` fixed to account for changes due to #71. Now using the new "original file" column while creating hard-links. - `e2e` overhauled, improved and well commented. +- total size now closely resemble `df` results (fix #75 @kopardev) +- files with future timestamps are handles correctly (fix #76, @kopardev) ## spacesavers2 0.10.2 diff --git a/README.md b/README.md index 90e8518..c31b6a2 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,13 @@ Welcome! `spacesavers2`: > New improved parallel implementation of [`spacesavers`](https://github.com/CCBR/spacesavers). `spacesavers` is soon to be decommissioned! -> Note: `spacesavers2` requires [python version 3.11](https://www.python.org/downloads/release/python-3110/) or later and the [xxhash](https://pypi.org/project/xxhash/) library. These dependencies are already installed on biowulf (as a conda env). The environment for running `spacesavers2` can get set up using: -> -> ```bash -> . "/data/CCBR_Pipeliner/db/PipeDB/Conda/etc/profile.d/conda.sh" && \ -> conda activate py311 -> ``` +> Note: `spacesavers2` requires [python version 3.11](https://www.python.org/downloads/release/python-3110/) or later and the [xxhash](https://pypi.org/project/xxhash/) library. These dependencies are already installed on biowulf (as a conda env). ## `spacesavers2` has the following Basic commands: - spacesavers2_catalog - spacesavers2_mimeo - spacesavers2_grubbers -- spacesavers2_blamematrix - spacesavers2_e2e - spacesavers2_usurp diff --git a/docs/assets/images/spacesavers2.png b/docs/assets/images/spacesavers2.png index a1e9a16ea4ffa5c4ac4c38b0b3bc6c514e1b8a9c..7b1d669cc645dea55ecf21cf0d16cf3697a789ab 100644 GIT binary patch literal 79102 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

eZ9ABJN0;i|7>h!4e1QWP#vbQu<)4 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 OZdPV?rY&dvQvVB_O#lf1 diff --git a/docs/blamematrix.md b/docs/blamematrix.md deleted file mode 100644 index 7868c96..0000000 --- a/docs/blamematrix.md +++ /dev/null @@ -1,49 +0,0 @@ -## spacesavers2_blamematrix - -This takes in the `allusers.mimeo.files.gz` generated by `spacesavers2_mimeo` and processes it to create a matrix with: - -- folder paths as row-names -- usernames as column-names -- duplicate bytes as values in the matrix - -Deleting these high-value duplicates first will have the biggest impact on the users overall digital footprint. - -### Inputs - -- `--filesgz` output file from `spacesavers2_mimeo`. -- `--level` depth at with to cutoff the output. -- `--humanreable` make the output human readable, that is, output in MiB, GiB, TiB, etc. instead of bytes. -- `--includezeros` include empty folders. -- `--outfile` path to the output file. - -```bash -% spacesavers2_blamematrix --help -usage: spacesavers2_blamematrix [-h] -f FILESGZ [-l LEVEL] [-r | --humanreable | --no-humanreable] [-z | --includezeros | --no-includezeros] [-o OUTFILE] [-v] - -spacesavers2_blamematrix: get per user duplicate sizes at a given folder level (default 3) - -options: - -h, --help show this help message and exit - -f FILESGZ, --filesgz FILESGZ - spacesavers2_mimeo prefix.allusers.mimeo.files.gz file - -l LEVEL, --level LEVEL - folder level to use for creating matrix - -r, --humanreable, --no-humanreable - sizes are printed in human readable format ... (default: Bytes) - -z, --includezeros, --no-includezeros - include folders where totalbytes is zero. - -o OUTFILE, --outfile OUTFILE - output tab-delimited file (default STDOUT) - -v, --version show program's version number and exit - -Version: - v0.10.2-dev -Example: - > spacesavers2_blamematrix -f /output/from/spacesavers2_mimeo/prefix.allusers.mimeo.files.gz -d 3 -o prefix.blamematrix.tsv -``` - -### Outputs - -Counts matrix with duplicate bytes per user per folder. - -> Note this can be used to generate a heatmap for quickly find folders with high duplicates and the user they belong to. diff --git a/mkdocs.yml b/mkdocs.yml index 3f8197e..3929983 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -102,6 +102,5 @@ nav: - catalog: catalog.md - mimeo: mimeo.md - grubbers: grubbers.md - - blamematrix: blamematrix.md - usurp: usurp.md - e2e: e2e.md diff --git a/spacesavers2_blamematrix b/spacesavers2_blamematrix deleted file mode 100755 index 9885c7c..0000000 --- a/spacesavers2_blamematrix +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 -import sys -import os -import gzip -import textwrap -import time - -from src.VersionCheck import version_check -from src.VersionCheck import __version__ - -version_check() - -# from src.FileDetails import FileDetails -from src.dfUnit import fgzblamer - -# from src.Summary import Summary -from src.utils import * -from datetime import date - -import argparse - - -def main(): - start = time.time() - scriptname = os.path.basename(__file__) - elog = textwrap.dedent( - """\ - Version: - {} - Example: - > spacesavers2_blamematrix -f /output/from/spacesavers2_mimeo/prefix.allusers.mimeo.files.gz -d 3 -o prefix.blamematrix.tsv - """.format( - __version__ - ) - ) - parser = argparse.ArgumentParser( - description="spacesavers2_blamematrix: get per user duplicate sizes at a given folder level (default 3)", - epilog=elog, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - parser.add_argument( - "-f", - "--filesgz", - dest="filesgz", - required=True, - type=str, - default=sys.stdin, - help="spacesavers2_mimeo prefix.allusers.mimeo.files.gz file", - ) - parser.add_argument( - "-l", - "--level", - dest="level", - required=False, - type=int, - default=3, - help="folder level to use for creating matrix", - ) - parser.add_argument( - "-r", - "--humanreable", - dest="humanreable", - required=False, - action=argparse.BooleanOptionalAction, - help="sizes are printed in human readable format ... (default: Bytes)", - ) - parser.add_argument( - "-z", - "--includezeros", - dest="includezeros", - required=False, - action=argparse.BooleanOptionalAction, - help="include folders where totalbytes is zero.", - ) - parser.add_argument( - "-o", - "--outfile", - dest="outfile", - required=False, - type=str, - help="output tab-delimited file (default STDOUT)", - ) - parser.add_argument("-v", "--version", action="version", version=__version__) - - print_with_timestamp( - start=start, scriptname=scriptname, string="version: {}".format(__version__) - ) - - global args - args = parser.parse_args() - - blamematrix = dict() - blamematrix["allusers"] = dict() - with gzip.open(os.path.join(args.filesgz), "rt") as filesgz: - for l in filesgz: - dfu = fgzblamer() - properly_set = dfu.set(l, args.level) - if not properly_set: - continue - for user in dfu.users: - if not user in blamematrix: - blamematrix[user] = dict() - for folder in dfu.bm[user]: - if not folder in blamematrix[user]: - blamematrix[user][folder] = 0 - if not folder in blamematrix["allusers"]: - blamematrix["allusers"][folder] = 0 - blamematrix[user][folder] += dfu.bm[user][folder] - blamematrix["allusers"][folder] += dfu.bm[user][folder] - - if args.outfile: - of = open(args.outfile, "w") - else: - of = sys.stdout - - users = list(blamematrix.keys()) - folders = list(blamematrix["allusers"].keys()) - users2 = ["folder"] - users2.extend(users) - outstr = "\t".join(users2) - of.write("%s\n"%(outstr)) - for folder in folders: - outlist = [] - outlist.append(str(folder)) - for user in users: - try: - hrsize = blamematrix[user][folder] - if args.humanreable: - hrsize = get_human_readable_size(hrsize) - except KeyError: - hrsize = "0" - outlist.append(str(hrsize)) - if blamematrix["allusers"][folder] == 0 : - if args.includezeros: - of.write("%s\n"%("\t".join(outlist))) - else: - of.write("%s\n"%("\t".join(outlist))) - if args.outfile: of.close() - print_with_timestamp(start=start, scriptname=scriptname, string="Done!") - - -if __name__ == "__main__": - main() diff --git a/spacesavers2_catalog b/spacesavers2_catalog index 0ba81ca..30b0d35 100755 --- a/spacesavers2_catalog +++ b/spacesavers2_catalog @@ -17,19 +17,17 @@ from pathlib import Path def task(f): - if not os.path.isfile(f): - return "" - else: - fd = FileDetails() - fd.initialize( - f, - buffersize=args.buffersize, - thresholdsize=args.ignoreheadersize, - tb=args.buffersize, - sed=sed, - bottomhash=args.bottomhash, - ) - return "%s" % (fd) + fd = FileDetails() + fd.initialize( + f, + buffersize=args.buffersize, + thresholdsize=args.ignoreheadersize, + tb=args.buffersize, + sed=sed, + bottomhash=args.bottomhash, + st_block_byte_size=args.st_block_byte_size, + ) + return "%s" % (fd) def main(): @@ -84,7 +82,7 @@ def main(): help="this sized header of the file is ignored before extracting buffer of buffersize for xhash creation (only for special extensions files) default = 1024 * 1024 * 1024 bytes", ) parser.add_argument( - "-s", + "-x", "--se", dest="se", required=False, @@ -92,6 +90,15 @@ def main(): default="bam,bai,bigwig,bw,csi", help="comma separated list of special extensions (default=bam,bai,bigwig,bw,csi)", ) + parser.add_argument( + "-s", + "--st_block_byte_size", + dest="st_block_byte_size", + required=False, + default=512, + type=int, + help="st_block_byte_size on current filesystem (default 512)", + ) parser.add_argument( "-o", "--outfile", @@ -120,7 +127,9 @@ def main(): folder = args.folder p = Path(folder) - files = p.glob("**/*") + files = [p] + files2 = p.glob("**/*") + files.extend(files2) if args.outfile: outfh = open(args.outfile, "w") diff --git a/spacesavers2_e2e b/spacesavers2_e2e index 26a3744..57d2bd2 100755 --- a/spacesavers2_e2e +++ b/spacesavers2_e2e @@ -13,8 +13,8 @@ source ${SCRIPT_DIR}/resources/argparse.bash || exit 1 argparse "$@" < ${outfile_catalog_log} 2> ${outfile_catalog_err} fi +sleep 60 + # spacesavers2_mimeo if [ "$?" == "0" ];then echo "Running spacesavers2_mimeo" -command -V ktImportText 2>/dev/null || module load kronatools || (>&2 echo "module kronatools could not be loaded"; exit 1) +command -V ktImportText 2>/dev/null || module load kronatools || (>&2 echo "module kronatools could not be loaded") spacesavers2_mimeo \ --catalog ${outfile_catalog} \ --outdir ${OUTFOLDER} \ --quota $QUOTA \ --duplicatesonly \ - --maxdepth 3 \ + --maxdepth $MAXDEPTH \ --p $prefix \ --kronaplot \ > ${outfile_mimeo_log} 2> ${outfile_mimeo_err} fi +sleep 60 + # spacesavers2_grubbers if [ "$?" == "0" ];then echo "Running spacesavers2_grubbers" && \ @@ -84,13 +88,4 @@ for filegz in `ls ${OUTFOLDER}/${prefix}*files.gz`;do done fi -# spacesavers2_blamematrix -if [ "$?" == "0" ];then -echo "Running spacesavers2_blamematrix" && \ -spacesavers2_blamematrix \ - --filesgz ${OUTFOLDER}/${prefix}.allusers.mimeo.files.gz \ - --level $LEVEL \ - --outfile ${outfile_blamematrix} \ - > ${outfile_blamematrix_log} 2> ${outfile_blamematrix_err} -fi -echo "Done!" +echo "Done!" \ No newline at end of file diff --git a/spacesavers2_grubbers b/spacesavers2_grubbers index 5bbd2a5..adc2c59 100755 --- a/spacesavers2_grubbers +++ b/spacesavers2_grubbers @@ -92,7 +92,7 @@ def main(): of = sys.stdout for fgitem in dups: - if fgitem.totalsize < top_limit: + if fgitem.totalsize <= top_limit: break saved += fgitem.totalsize of.write("%s\n"%(fgitem)) @@ -100,11 +100,11 @@ def main(): if args.outfile: of.close() - saved = get_human_readable_size(saved) + hrsaved = get_human_readable_size(saved) print_with_timestamp( start=start, scriptname=scriptname, - string="Deleting top grubbers will save {}!".format(saved), + string="Deleting top grubbers will save {} [ {} Bytes ] !".format(hrsaved,saved), ) print_with_timestamp(start=start, scriptname=scriptname, string="Done!") diff --git a/spacesavers2_mimeo b/spacesavers2_mimeo index 04a61e3..822c4ee 100755 --- a/spacesavers2_mimeo +++ b/spacesavers2_mimeo @@ -24,19 +24,6 @@ from datetime import date import argparse -def check_terminal_list(p,tlist): - outcome = -1 # append path to tlist - for i,p2 in enumerate(tlist): - if p.len < p2.len: - if p.path in p2.path: - outcome = -2 # path already in tlist - return outcome - else: - if p2.path in p.path: - outcome = i - return outcome - return outcome - def process_hh( uid, hashhash, @@ -45,7 +32,8 @@ def process_hh( maxdepth, uid2uname, gid2gname, - peruser_perfolder_summaries, + perfolder_summaries, + perfolder_dups, user_output, ): for h in hashhash.keys(): @@ -66,63 +54,42 @@ def process_hh( foldest = hashhash[h].flist[oldest_index] user_owns_original = False if foldest.uid == uid or 0 == uid : user_owns_original = True - uid_dup_file_index = [] - if hashhash[h].ndup_inode > 1: # there are duplicate inodes and hence there are duplicate files - inodes_already_summerized = list() + uid_file_index = list(filter(lambda x:x!=oldest_index,uid_file_index)) # remove oldest if present in list + inodes_already_summerized = list() + if hashhash[h].ndup_files > 0: # we have duplicates for i in uid_file_index: f = hashhash[h].flist[i] + fpath = f.apath + parent = fpath.parent fpaths = f.get_paths(mindepth, maxdepth) - if ( - i == oldest_index - ): # its the original file ... not a duplicate + if f.inode in inodes_already_summerized: # it is a hardlink for p in fpaths: - peruser_perfolder_summaries[p].nnondup_files += 1 - peruser_perfolder_summaries[p].non_dup_Bytes.append(f.size) - peruser_perfolder_summaries[p].non_dup_ages.append(f.mtime) - inodes_already_summerized.append(f.inode) # scenario where original already has a hard-link + perfolder_summaries[p].ndup_files += 1 else: - uid_dup_file_index.append(i) - # has the inode already been summarized - if f.inode in inodes_already_summerized: - for p in fpaths: - peruser_perfolder_summaries[p].ndup_files += 1 - else: - inodes_already_summerized.append(f.inode) - for p in fpaths: - peruser_perfolder_summaries[p].ndup_files+=1 - peruser_perfolder_summaries[p].dup_Bytes.append(f.size) - peruser_perfolder_summaries[p].dup_ages.append(f.mtime) - else: - # ndup_inode == 1 .. meaning there are no duplicate inodes .. can still have multiple hard linked files - # only count 1 file/hardlink for summary - i = uid_file_index[0] - f = hashhash[h].flist[i] - fpaths = f.get_paths(mindepth, maxdepth) - for p in fpaths: - peruser_perfolder_summaries[p].nnondup_files += 1 - peruser_perfolder_summaries[p].non_dup_Bytes.append(f.size) - peruser_perfolder_summaries[p].non_dup_ages.append(f.mtime) - if args.duplicatesonly: - if len(uid_dup_file_index) > 0: # this user has some duplicates - out_index = [oldest_index] - out_index.extend(uid_dup_file_index) - user_output.write( - "{}\n".format( - hashhash[h].str_with_name( - uid2uname, gid2gname, out_index - ) - ) - ) - else: - out_index = [] - if user_owns_original == False: - out_index.append(oldest_index) - out_index.extend(uid_file_index) - user_output.write( - "{}\n".format( - hashhash[h].str_with_name(uid2uname, gid2gname, out_index) - ) + inodes_already_summerized.append(f.inode) + if not parent in perfolder_dups: + perfolder_dups[fpath.parent] = 0 + perfolder_dups[fpath.parent] += f.calculated_size + for p in fpaths: + perfolder_summaries[p].ndup_files+=1 + perfolder_summaries[p].dup_Bytes.append(f.calculated_size) + perfolder_summaries[p].dup_ages.append(f.mtime) + else: # we only have 1 original file + if user_owns_original: + fpaths = foldest.get_paths(mindepth, maxdepth) + for p in fpaths: + perfolder_summaries[p].nnondup_files += 1 + perfolder_summaries[p].non_dup_Bytes.append(foldest.calculated_size) + perfolder_summaries[p].non_dup_ages.append(foldest.mtime) + out_index = [] + out_index.append(oldest_index) + out_index.extend(uid_file_index) + if args.duplicatesonly and len(out_index)==1: continue + user_output.write( + "{}\n".format( + hashhash[h].str_with_name(uid2uname, gid2gname, out_index) ) + ) def main(): @@ -241,19 +208,24 @@ def main(): start=start, scriptname=scriptname, string="Reading in catalog file..." ) set_complete = True + folder_info = dict() with open(args.catalog) as catalog: for l in catalog: fd = FileDetails() set_complete = fd.set(l) if not set_complete: continue - if fd.issyml: + if fd.fld != "d" and fd.fld !="f": # not a file or folder continue # ignore all symlinks users.add(fd.uid) groups.add(fd.gid) - path_lens.add(get_file_depth(fd.apath)) + path_lens.add(fd.get_depth()) for p in fd.get_paths_at_all_depths(): paths.add(p) + if fd.fld == "d": + if not fd.apath in folder_info: + folder_info[fd.apath] = fd + continue hash = fd.xhash_top + "#" + fd.xhash_bottom if hash == "#": # happens when file cannot be read sys.stderr.write( @@ -304,8 +276,13 @@ def main(): scriptname=scriptname, string="Total Number of users: %d" % len(users), ) - + blamematrixtsv = os.path.join( + os.path.abspath(args.outdir), "blamematrix.tsv" + ) + blamematrix = dict() + all_blamematrix_paths = set() for uid in users: + blamematrix[uid] = dict() print_with_timestamp( start=start, scriptname=scriptname, @@ -337,9 +314,22 @@ def main(): with gzip.open(useroutputpath, "wt") as user_output, open( summaryfilepath, "a" ) as user_summary: - peruser_perfolder_summaries = dict() + perfolder_summaries = dict() + perfolder_dups = dict() for p in paths: - peruser_perfolder_summaries[p] = Summary(p) + perfolder_summaries[p] = Summary(p) + if not p in folder_info: + folder_info[p] = FileDetails() + folder_info[p].initialize(p) + fd = folder_info[p] + for p2 in fd.get_paths(mindepth,maxdepth): + if not p2 in folder_info: + folder_info[p2] = FileDetails() + folder_info[p2].initialize(p2) + fd2 = folder_info[p2] + if fd2.uid == uid or uid == 0: + perfolder_summaries[p2].folder_Bytes += fd.calculated_size + hashhashsplits = dict() # dict to collect instances where the files are NOT duplicates has same hashes but different sizes (and different inodes) ... new suffix is added to bottomhash .."_iterator" process_hh( uid, @@ -349,7 +339,8 @@ def main(): maxdepth, uid2uname, gid2gname, - peruser_perfolder_summaries, + perfolder_summaries, + perfolder_dups, user_output, ) if len(hashhashsplits) != 0: @@ -362,42 +353,35 @@ def main(): maxdepth, uid2uname, gid2gname, - peruser_perfolder_summaries, + perfolder_summaries, + perfolder_dups, user_output, ) del hashhashsplitsdummy del hashhashsplits for p in paths: - peruser_perfolder_summaries[p].update_scores(quota) - user_summary.write(f"{peruser_perfolder_summaries[p]}\n") + perfolder_summaries[p].update_scores(quota) + user_summary.write(f"{perfolder_summaries[p]}\n") + for p in perfolder_summaries: + dummy = FileDetails() + dummy.initialize(p) + if dummy.get_depth() == mindepth + 1: + all_blamematrix_paths.add(p) + blamematrix[uid][p] = sum(perfolder_summaries[p].dup_Bytes) if args.kronaplot: - terminal_paths = [] - with open(summaryfilepath,'r') as infile: - count = 0 - for l in infile: - l = l.strip().split("\t") - count += 1 - if count==1: - continue #header - if count==2: - terminal_paths.append(pathlen(l[0],int(l[2]))) - continue - p = pathlen(l[0],int(l[2])) - outcome = check_terminal_list(p,terminal_paths) - if outcome == -1: # new ... append - terminal_paths.append(p) - elif outcome == -2: # already in list .. move on - continue - elif outcome > -1: # better than current one in list ... swap - terminal_paths[outcome] = p + print_with_timestamp( + start=start, + scriptname=scriptname, + string="Creating Kronachart for user: %s" % (uid2uname[uid]), + ) with open(kronatsv,'w') as ktsv: - for p in terminal_paths: - path = p.path + for p in perfolder_dups: + path = str(p) path = path.replace('/','\t') path = path.replace('\t\t','\t') - if p.dupbytes != 0 : - ktsv.write("%d\t%s\n"%(p.dupbytes,path)) + if perfolder_dups[p] != 0: + ktsv.write("%d\t%s\n"%(perfolder_dups[p],path)) if ktImportText_in_path: cmd = "ktImportText %s -o %s"%(kronatsv,kronahtml) srun = subprocess.run(cmd,shell=True, capture_output=True, text=True) @@ -405,8 +389,31 @@ def main(): sys.stderr.write("%s\n"%(srun.stderr)) del hashhash - print_with_timestamp(start=start, scriptname=scriptname, string="Finished!") + print_with_timestamp( + start=start, + scriptname=scriptname, + string="Creating Blamematrix", + ) + with open(blamematrixtsv,'w') as btsv: + outlist = ["path"] + uids = list(blamematrix.keys()) + uids.sort() + for uid in uids: + outlist.append(uid2uname[uid]) + btsv.write("\t".join(outlist)+"\n") + for p in all_blamematrix_paths: + outlist = [str(p)] + s = 0 + for uid in uids: + if p in blamematrix[uid]: + s += blamematrix[uid][p] + outlist.append(str(blamematrix[uid][p])) + else: + outlist.append(str(0)) + if s != 0 : btsv.write("\t".join(outlist)+"\n") + + print_with_timestamp(start=start, scriptname=scriptname, string="Finished!") if __name__ == "__main__": main() diff --git a/spacesavers2_usurp b/spacesavers2_usurp index 1ed5cac..2c79c9a 100755 --- a/spacesavers2_usurp +++ b/spacesavers2_usurp @@ -11,10 +11,6 @@ from src.VersionCheck import __version__ version_check() -# from src.FileDetails import FileDetails -from src.dfUnit import fgzblamer - -# from src.Summary import Summary from src.utils import * from datetime import date @@ -81,7 +77,7 @@ def main(): lhash = l[0] if args.hash in lhash: original_copy = Path(l[4].strip('"')) - dupfiles = l[5].split(";") + dupfiles = l[5].split("##") print_with_timestamp( start=start, scriptname=scriptname, diff --git a/src/FileDetails.py b/src/FileDetails.py index 1204257..ce7a4db 100644 --- a/src/FileDetails.py +++ b/src/FileDetails.py @@ -9,8 +9,8 @@ except ImportError: exit(f"{sys.argv[0]} requires xxhash module") -THRESHOLDSIZE = 1024 * 1024 * 1024 -BUFFERSIZE = 128 * 1024 +THRESHOLDSIZE = 1024 * 1024 * 1024 # 1 MiB +BUFFERSIZE = 128 * 1024 # 128 KiB TB = THRESHOLDSIZE+BUFFERSIZE SEED = 20230502 MINDEPTH = 3 @@ -21,14 +21,33 @@ SED[se]=1 def convert_time_to_age(t): - currenttime=int(time.time()) - return int((currenttime - t)/86400)+1 + currenttime = int(time.time()) + age = int((currenttime - t)/86400)+1 + if age < 0: age = 0 + return age + +def get_type(p): + x = "u" # unknown + if not p.exists(): + x = "a" # absent + return x + if p.is_symlink(): + x = "l" # link or symlink + return x + if p.is_dir(): + x = "d" # directory + return x + if p.is_file(): + x = "f" # file + return x + return x class FileDetails: def __init__(self): self.apath = "" # absolute path of file - self.issyml = False + self.fdl = "u" # is it file or directory or link or unknown or absent ... values are f d l u a self.size = -1 + self.calculated_size = -1 self.dev = -1 self.inode = -1 self.nlink = -1 @@ -40,12 +59,13 @@ def __init__(self): self.xhash_top = "" self.xhash_bottom = "" - def initialize(self,f,thresholdsize=THRESHOLDSIZE, buffersize=BUFFERSIZE, tb=TB, sed=SED, bottomhash=False): + def initialize(self,f,thresholdsize=THRESHOLDSIZE, buffersize=BUFFERSIZE, tb=TB, sed=SED, bottomhash=False,st_block_byte_size=512): self.apath = Path(f).absolute() # path is of type PosixPath ext = self.apath.suffix - self.issyml = self.apath.is_symlink() # is a symbolic link - st = os.stat(self.apath) # gather all stats + self.fld = get_type(self.apath) # get if it is a file or directory or link or unknown or absent + st = self.apath.stat(follow_symlinks=False) # gather stat results self.size = st.st_size # size in bytes + self.calculated_size = st.st_blocks * st_block_byte_size # st_blocks gives number of 512 bytes blocks used self.dev = st.st_dev # Device id self.inode = st.st_ino # Inode self.nlink = st.st_nlink # number of hardlinks @@ -54,39 +74,40 @@ def initialize(self,f,thresholdsize=THRESHOLDSIZE, buffersize=BUFFERSIZE, tb=TB, self.ctime = convert_time_to_age(st.st_ctime) # creation time self.uid = st.st_uid # user id self.gid = st.st_gid # group id - try: - with open(self.apath,'rb') as fh: - if ext in sed: - if self.size > tb: - data = fh.read(thresholdsize) - data = fh.read(buffersize) - self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() - if bottomhash: - fh.seek(-1 * buffersize,2) + if self.fld == "f": + try: + with open(self.apath,'rb') as fh: + if ext in sed: + if self.size > tb: + data = fh.read(thresholdsize) data = fh.read(buffersize) - self.xhash_bottom = xxhash.xxh128(data,seed=SEED).hexdigest() + self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() + if bottomhash: + fh.seek(-1 * buffersize,2) + data = fh.read(buffersize) + self.xhash_bottom = xxhash.xxh128(data,seed=SEED).hexdigest() + else: + self.xhash_bottom = self.xhash_top else: + data = fh.read() + self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() self.xhash_bottom = self.xhash_top else: - data = fh.read() - self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() - self.xhash_bottom = self.xhash_top - else: - if self.size > buffersize: - data = fh.read(buffersize) - self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() - if bottomhash: - fh.seek(-1 * buffersize,2) + if self.size > buffersize: data = fh.read(buffersize) - self.xhash_bottom = xxhash.xxh128(data,seed=SEED).hexdigest() + self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() + if bottomhash: + fh.seek(-1 * buffersize,2) + data = fh.read(buffersize) + self.xhash_bottom = xxhash.xxh128(data,seed=SEED).hexdigest() + else: + self.xhash_bottom = self.xhash_top else: + data = fh.read() + self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() self.xhash_bottom = self.xhash_top - else: - data = fh.read() - self.xhash_top = xxhash.xxh128(data,seed=SEED).hexdigest() - self.xhash_bottom = self.xhash_top - except: - sys.stderr.write("spacesavers2:{}:File cannot be read:{}\n".format(self.__class__.__name__,str(self.apath))) + except: + sys.stderr.write("spacesavers2:{}:File cannot be read:{}\n".format(self.__class__.__name__,str(self.apath))) def set(self,ls_line): original_ls_line=ls_line @@ -105,9 +126,9 @@ def set(self,ls_line): self.nlink = int(ls_line.pop(-1)) self.inode = int(ls_line.pop(-1)) self.dev = int(ls_line.pop(-1)) + self.calculated_size = int(ls_line.pop(-1)) self.size = int(ls_line.pop(-1)) - issyml = ls_line.pop(-1) - self.issyml = issyml == 'True' + self.fld = ls_line.pop(-1) self.apath = Path(";".join(ls_line)) # sometimes filename have ";" in them ... hence this! return True except: @@ -119,8 +140,9 @@ def str_with_name(self,uid2uname,gid2gname):# method for printing output in mime # return_str = "\"%s\";"%(self.apath) # path may have newline char which should not be interpretted as new line char return_str = "\"%s\";"%(str(self.apath).encode('unicode_escape').decode('utf-8')) - # return_str += "%s;"%(self.issyml) + return_str += "%s;"%(self.fld) return_str += "%d;"%(self.size) + return_str += "%d;"%(self.calculated_size) return_str += "%d;"%(self.dev) return_str += "%d;"%(self.inode) return_str += "%d;"%(self.nlink) @@ -137,8 +159,9 @@ def __str__(self): # return_str = "\"%s\";"%(self.apath) # path may have newline char which should not be interpretted as new line char return_str = "\"%s\";"%(str(self.apath).encode('unicode_escape').decode('utf-8')) - return_str += "%s;"%(self.issyml) + return_str += "%s;"%(self.fld) return_str += "%d;"%(self.size) + return_str += "%d;"%(self.calculated_size) return_str += "%d;"%(self.dev) # device id return_str += "%d;"%(self.inode) return_str += "%d;"%(self.nlink) @@ -151,12 +174,27 @@ def __str__(self): return_str += "%s;"%(self.xhash_bottom) return return_str - def get_paths_at_all_depths(self): # for files - return self.apath.parents[:-1] # remove the last one ... which will be '/' + def get_paths_at_all_depths(self): # for files and folders + p = self.apath + paths = [] + if self.fld == "d": + paths.append(p) + paths.extend(p.parents[:-1]) # remove the last one ... which will be '/' + return paths def get_paths(self,mindepth,maxdepth): - parents = list(self.apath.parents[0:-1]) - parents = list(filter(lambda x:get_folder_depth(x) <= maxdepth,parents)) - parents = list(filter(lambda x:get_folder_depth(x) >= mindepth,parents)) - return parents + paths = self.get_paths_at_all_depths() + paths = list(filter(lambda x:get_folder_depth(x) <= maxdepth,paths)) + paths = list(filter(lambda x:get_folder_depth(x) >= mindepth,paths)) + return paths + def get_depth(self): + p = self.apath + try: + if p.is_dir(): # folder + return len(list(p.parents)) + else: # file + return len(list(p.parents)) - 1 + except: + print('get_file_depth error for file:"{}", type:{}'.format(path, type(path))) + exit() \ No newline at end of file diff --git a/src/Summary.py b/src/Summary.py index fe5ab54..28435fa 100644 --- a/src/Summary.py +++ b/src/Summary.py @@ -33,6 +33,7 @@ def __init__(self,path): self.ndup_files = 0 self.non_dup_Bytes = [] self.dup_Bytes = [] + self.folder_Bytes = 0 self.non_dup_ages = [] self.dup_ages = [] self.non_dup_age_scores = [] @@ -71,7 +72,7 @@ def print_header(self): def __str__(self): dup_Bytes = sum(self.dup_Bytes) - tot_Bytes = sum(self.non_dup_Bytes) + dup_Bytes + tot_Bytes = sum(self.non_dup_Bytes) + dup_Bytes + self.folder_Bytes try: dup_mean_age = sum(self.dup_ages)/len(self.dup_ages) except ZeroDivisionError: diff --git a/src/dfUnit.py b/src/dfUnit.py index 6be1a68..73c9e1b 100644 --- a/src/dfUnit.py +++ b/src/dfUnit.py @@ -3,7 +3,7 @@ def get_filename_from_fgzlistitem(string): string = string.strip().split(";")[:-1] - for i in range(9): + for i in range(11): dummy = string.pop(-1) filename = ";".join(string) return filename @@ -12,12 +12,13 @@ def get_filename_from_fgzlistitem(string): class dfUnit: def __init__(self,hash): self.hash = hash # typically hash_top + "#" + hash_bottom - self.flist = [] # list of _ls files with the same hash - self.fsize = -1 # size of each file + self.flist = [] # list of catalog files with the same hash + self.fsize = -1 # calculated size of each file self.ndup = -1 # files in flist with same size, but different inode (they already have the same hash) self.ndup_files = -1 # number of duplicate files ... used for counting duplicate files self.ndup_inode = -1 # number of duplicate inodes ... used for counting duplicate bytes self.size_set = set() # set of unique sizes ... if len(size_set) then split is required + self.calculated_size_list = [] self.uid_list = [] # list of uids of files added self.inode_list = [] # list of inodes of files added self.oldest_inode = -1 # oldest_ ... is for the file which is NOT the duplicate or is the original @@ -33,6 +34,7 @@ def add_fd(self,fd): self.flist.append(fd) # add size if not already present self.size_set.add(fd.size) + self.calculated_size_list.append(fd.calculated_size) # add uid self.uid_list.append(fd.uid) # add inode @@ -71,7 +73,7 @@ def compute(self,hashhashsplits): self.ndup = len(self.inode_list) - 1 #ndup is zero if same len(size_set)==1 and len(inode_list)==1 self.ndup_inode = len(set(self.inode_list)) - 1 self.ndup_files = len(self.inode_list) - 1 - self.fsize = self.flist[0].size + self.fsize = self.flist[0].calculated_size return split_required def get_user_file_index(self,uid): @@ -87,10 +89,10 @@ def get_user_file_index(self,uid): def __str__(self): - return "{0} : {1} {2} {3}".format(self.hash, self.ndup, self.fsize,"##".join(map(lambda x:str(x),self.flist))) + return "{0} : {1} {2} {3}".format(self.hash, self.ndup_inode, self.fsize,"##".join(map(lambda x:str(x),self.flist))) def str_with_name(self,uid2uname, gid2gname,findex): - return "{0} : {1} {2} {3}".format(self.hash, self.ndup, self.fsize,"##".join(map(lambda x:x.str_with_name(uid2uname,gid2gname),[self.flist[i] for i in findex]))) + return "{0} : {1} {2} {3}".format(self.hash, self.ndup_inode, self.fsize,"##".join(map(lambda x:x.str_with_name(uid2uname,gid2gname),[self.flist[i] for i in findex]))) class fgz: # used by grubber @@ -109,10 +111,10 @@ def __str__(self): outstring=[] outstring.append(str(self.hash)) outstring.append(str(self.ndup)) - outstring.append(get_human_readable_size(self.totalsize)) - outstring.append(get_human_readable_size(self.filesize)) + outstring.append(str(self.totalsize)) + outstring.append(str(self.filesize)) outstring.append(get_filename_from_fgzlistitem(self.of)) - outstring.append(";".join(map(lambda x:get_filename_from_fgzlistitem(x),self.fds))) + outstring.append("##".join(map(lambda x:get_filename_from_fgzlistitem(x),self.fds))) return "\t".join(outstring) # return "{0} {1} {2} {3} {4}".format(self.hash,self.ndup,get_human_readable_size(self.totalsize), get_human_readable_size(self.filesize), ";".join(map(lambda x:get_filename_from_fgzlistitem(x),self.fds))) # return "{0} {1} {2} {3} {4}".format(self.hash,self.ndup,self.totalsize, self.filesize, ";".join(map(lambda x:get_filename_from_fgzlistitem(x),self.fds))) @@ -135,90 +137,7 @@ def set(self,inputline): self.ndup = total_ndup # these are user number of duplicates/files self.of = fds.pop(0) # one file is the original self.fds = fds # others are dupicates - inodes_set = set() - for f in fds: - l = f.split(";") - inodes_set.add(l[-7]) - self.totalsize = self.ndup * self.filesize - return True - except: - sys.stderr.write("spacesavers2:{0}:files.gz Do not understand line:{1} with {2} elements.\n".format(self.__class__.__name__,original_line,len(inputline))) - # exit() - return False - - -class FileDetails2: - def __init__(self): - self.apath = "" - self.size = -1 - self.dev = -1 - self.inode = -1 - self.nlink = -1 - self.mtime = -1 - self.uid = -1 - self.gid = -1 - self.uname = "" - self.gname = "" - - def set(self,fgzline): - original_fgzline=fgzline - # print(ls_line) - try: - fgzline = fgzline.strip().replace("\"","").split(";")[:-1] - if len(fgzline) < 10: - raise Exception("Less than 10 items in the line.") - self.gname = fgzline.pop(-1) - self.uname = fgzline.pop(-1) - self.gid = int(fgzline.pop(-1)) - self.uid = int(fgzline.pop(-1)) - self.mtime = int(fgzline.pop(-1)) - self.nlink = int(fgzline.pop(-1)) - self.inode = int(fgzline.pop(-1)) - self.dev = int(fgzline.pop(-1)) - self.size = int(fgzline.pop(-1)) - apath = ";".join(fgzline) - apath = apath.strip("\"") - self.apath = Path(apath) # sometimes filename have ";" in them ... hence this! - return True - except: - sys.stderr.write("spacesavers2:{0}:catalog Do not understand line:\"{1}\" with {2} elements.\n".format(self.__class__.__name__,original_fgzline,len(fgzline))) - # exit() - return False - -class fgzblamer: # used by blamematrix - def __init__(self): - self.hash = "" - self.ndup = -1 - self.users = set() - self.folders = set() - self.bm = dict() - self.fds = [] - - def set(self,inputline,depth): - original_line = inputline - try: - inputline = inputline.strip().split(" ") - if len(inputline) < 5: - raise Exception("Less than 5 items in the line.") - self.hash = inputline.pop(0) - dummy = inputline.pop(0) - self.ndup = int(inputline.pop(0)) - if self.ndup == 0 or self.ndup == 1: return False - self.filesize = int(inputline.pop(0)) - full_fds = " ".join(inputline) - fds = full_fds.split("##") - for f in fds: - fd = FileDetails2() - fd.set(f) - self.users.add(fd.uname) - fad=get_folder_at_depth(fd.apath,depth) - self.folders.add(fad) - if not fd.uname in self.bm: - self.bm[fd.uname] = dict() - if not fad in self.bm[fd.uname]: - self.bm[fd.uname][fad] = 0 - self.bm[fd.uname][fad] += self.filesize - self.fds = [] + self.totalsize = total_ndup * self.filesize return True except: sys.stderr.write("spacesavers2:{0}:files.gz Do not understand line:{1} with {2} elements.\n".format(self.__class__.__name__,original_line,len(inputline))) diff --git a/src/utils.py b/src/utils.py index 279046d..68c4b51 100644 --- a/src/utils.py +++ b/src/utils.py @@ -72,18 +72,6 @@ def get_folder_depth(path): return len(list(path.parents)) -def get_file_depth(path): -# example -# >>> len(list(Path("/f1/f2/f3/f4/a.xyz").absolute().parents))-1 -# 4 -# a.k.a. file a.xyz is 4 folders deep - try: - return len(list(path.parents)) - 1 - except: - print('get_file_depth error for file:"{}", type:{}'.format(path, type(path))) - exit() - - def get_timestamp(start): e = time.time() return "%08.2fs" % (e - start) From fe89fac5730cfe05c6956dcb9d5ffae33eae5b52 Mon Sep 17 00:00:00 2001 From: kopardev Date: Wed, 7 Feb 2024 15:49:07 -0500 Subject: [PATCH 17/22] more fixes to not count files with the same inode as duplicates logic fix. --- docs/mimeo.md | 12 ++++++++++-- spacesavers2_mimeo | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/mimeo.md b/docs/mimeo.md index d3d598f..2336721 100644 --- a/docs/mimeo.md +++ b/docs/mimeo.md @@ -46,7 +46,9 @@ Example: ### Outputs -After completion of run, `spacesavers2_mimeo` creates `*.mimeo.files.gz` (list of files per user + one "allusers" file) and `.summary.txt` (overall stats at various depths) files in the provided output folder. if `-k` is provided (and ktImportText from [kronatools](https://github.com/marbl/Krona/wiki/KronaTools) is in PATH) then krona specific TSV and HTML pages are also generated. Here are the details: +After completion of run, `spacesavers2_mimeo` creates `*.mimeo.files.gz` (list of files per user + one "allusers" file) and `.summary.txt` (overall stats at various depths) files in the provided output folder. if `-k` is provided (and ktImportText from [kronatools](https://github.com/marbl/Krona/wiki/KronaTools) is in PATH) then krona specific TSV and HTML pages are also generated. It also generates a `blamematrix.tsv` file with folders on rows and users on columns with duplicate bytes per-folder-per-user. This file can be used to create a "heatmap" to pinpoint folder with highest duplicates overall as well as on a per-user basis. + +Here are the details: #### Duplicates @@ -100,4 +102,10 @@ For columns 10 through 13, the same logic is used as [spacesavers](https://ccbr. #### KronaTSV and KronaHTML - KronaTSV is tab-delimited with first column showing the number of duplicate bytes and every subsequent column giving the folder depths. -- ktImportText is then used to convert the KronaTSV to KronaHTML which can be shared easily and only needs a HTML5 supporting browser for viewing. \ No newline at end of file +- ktImportText is then used to convert the KronaTSV to KronaHTML which can be shared easily and only needs a HTML5 supporting browser for viewing. + +#### Blamematrix + +- rows are folders as 1 level deeper than the "mindepth" +- columns are all individual usernames, plus an "allusers" column +- only duplicate-bytes are reported \ No newline at end of file diff --git a/spacesavers2_mimeo b/spacesavers2_mimeo index 822c4ee..f5fc2e5 100755 --- a/spacesavers2_mimeo +++ b/spacesavers2_mimeo @@ -55,7 +55,7 @@ def process_hh( user_owns_original = False if foldest.uid == uid or 0 == uid : user_owns_original = True uid_file_index = list(filter(lambda x:x!=oldest_index,uid_file_index)) # remove oldest if present in list - inodes_already_summerized = list() + inodes_already_summerized = [foldest.inode] if hashhash[h].ndup_files > 0: # we have duplicates for i in uid_file_index: f = hashhash[h].flist[i] From bdee6d3d5007e057dd10109942fe674b4afdc0a5 Mon Sep 17 00:00:00 2001 From: Kelly Sovacool Date: Wed, 7 Feb 2024 16:00:58 -0500 Subject: [PATCH 18/22] docs: INFOLDER argument is called FOLDER in e2e CLI --- docs/e2e.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/e2e.md b/docs/e2e.md index a47b59e..43f54a9 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -18,7 +18,7 @@ End-to-end run of spacesavers2 options: -h, --help show this help message and exit - -i INFOLDER, --infolder INFOLDER + -f FOLDER, --folder FOLDER Folder to run spacesavers_ls on. -p THREADS, --threads THREADS number of threads to use From 56b8cadda1c3a0791b5e0671853c34faf5d9e81b Mon Sep 17 00:00:00 2001 From: Kelly Sovacool Date: Wed, 7 Feb 2024 17:09:09 -0500 Subject: [PATCH 19/22] docs: improve changelog notes for #74 --- CHANGELOG.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f264d..5c51a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,23 @@ ### New features -- adding `requirements.txt` for easy creation of environment in "spacesavers2" docker (#68, @kopardev) +- Add `requirements.txt` for easy creation of environment in "spacesavers2" docker (#68, @kopardev) - `grubbers` has new `--outfile` argument. - `blamematrix` has now been moved into `mimeo`. - `mimeo` files.gz always includes the original file as the first one in the filelist. - `mimeo` now has kronatools compatible output. ktImportText is also run if in PATH to generate HTML report for duplicates only. (#46, @kopardev) -- documentation updated. +- Update documentation. ### Bug fixes -- `grubbers` `--limit` can be < 1 GiB (float) (#70, @kopardev) -- `grubbers` output file format changed. New original file column added. Original file is required by `usurp` -- `mimeo` `--duplicateonly` logic fix (#71, @kopardev) -- `blamematrix` fixed to account for changes due to #71 -- `usurp` fixed to account for changes due to #71. Now using the new "original file" column while creating hard-links. - `e2e` overhauled, improved and well commented. -- total size now closely resemble `df` results (fix #75 @kopardev) -- files with future timestamps are handles correctly (fix #76, @kopardev) +- `grubbers` `--limit` can be < 1 GiB (float) (#70, @kopardev) +- `grubbers` output file format changed. New original file column added. Original file is required by `usurp`. +- `mimeo` `--duplicateonly` now correctly handles duplicates owned by different UIDs. (#71, @kopardev) + - Update `blamematrix` and to account for corrected duplicate handling in `mimeo`. + - `usurp` now uses the new "original file" column while creating hard-links. +- Total size now closely resembles `df` results (fix #75 @kopardev) +- Files with future timestamps are handled correctly (fix #76, @kopardev) ## spacesavers2 0.10.2 From 1e57b5d05daa9f9e59287acab1267b4fc88083aa Mon Sep 17 00:00:00 2001 From: Kelly Sovacool Date: Wed, 7 Feb 2024 17:10:42 -0500 Subject: [PATCH 20/22] docs: usurp's change is due to grubbers output change for hardlinks --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c51a1a..b65cf73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - `grubbers` output file format changed. New original file column added. Original file is required by `usurp`. - `mimeo` `--duplicateonly` now correctly handles duplicates owned by different UIDs. (#71, @kopardev) - Update `blamematrix` and to account for corrected duplicate handling in `mimeo`. - - `usurp` now uses the new "original file" column while creating hard-links. +- `usurp` now uses the new "original file" column from `grubbers` while creating hard-links. - Total size now closely resembles `df` results (fix #75 @kopardev) - Files with future timestamps are handled correctly (fix #76, @kopardev) From f72923ad5b341c4691826e83ed698ede256018e2 Mon Sep 17 00:00:00 2001 From: kopardev Date: Wed, 7 Feb 2024 18:54:53 -0500 Subject: [PATCH 21/22] docs: more updates/corrections to e2e.md --- docs/e2e.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/e2e.md b/docs/e2e.md index 43f54a9..d81e7aa 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -19,11 +19,15 @@ End-to-end run of spacesavers2 options: -h, --help show this help message and exit -f FOLDER, --folder FOLDER - Folder to run spacesavers_ls on. + Folder to run spacesavers_catalog on. -p THREADS, --threads THREADS number of threads to use + -d MAXDEPTH, --maxdepth MAXDEPTH + maxdepth for mimeo + -l LIMIT, --limit LIMIT + limit for running spacesavers_grubbers -q QUOTA, --quota QUOTA total size of the volume (default = 200 for /data/CCBR) -o OUTFOLDER, --outfolder OUTFOLDER - Folder where all spacesavers_finddup output files will be saved + Folder where all spacesavers_e2e output files will be saved ``` \ No newline at end of file From 615f1caf4b85a51413e71c5d6722266ec738e94f Mon Sep 17 00:00:00 2001 From: kopardev Date: Wed, 7 Feb 2024 18:58:38 -0500 Subject: [PATCH 22/22] refac: adding prefix to blamematrix output file --- spacesavers2_mimeo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacesavers2_mimeo b/spacesavers2_mimeo index f5fc2e5..597f0d5 100755 --- a/spacesavers2_mimeo +++ b/spacesavers2_mimeo @@ -277,7 +277,7 @@ def main(): string="Total Number of users: %d" % len(users), ) blamematrixtsv = os.path.join( - os.path.abspath(args.outdir), "blamematrix.tsv" + os.path.abspath(args.outdir), args.prefix + "." + "blamematrix.tsv" ) blamematrix = dict() all_blamematrix_paths = set()