Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mp_hashdirectory.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Returns a unique hash for each file in a directory
4 @details Hashes each file in each directory, and then hashes the hashes to
5 create a hash for each directory also.
6
7 This makes use of the new `hashing_file()` and `hashing` functions, available
8 since 9.4m6. Interestingly, those functions can be used in pure macro, eg:
9
10 %put %sysfunc(hashing_file(md5,/path/to/file.blob,0));
11
12 Actual usage:
13
14 %let fpath=/some/directory;
15
16 %mp_hashdirectory(&fpath,outds=myhash,maxdepth=2)
17
18 data _null_;
19 set work.myhash;
20 put (_all_)(=);
21 run;
22
23 Whilst files are hashed in their entirety, the logic for creating a folder
24 hash is as follows:
25
26 @li Sort the files by filename (case sensitive, uppercase then lower)
27 @li Take the first 100 hashes, concatenate and hash
28 @li Concatenate this hash with another 100 hashes and hash again
29 @li Continue until the end of the folder. This is the folder hash
30 @li If a folder contains other folders, start from the bottom of the tree -
31 the folder hashes cascade upwards so you know immediately if there is a
32 change in a sub/sub directory
33 @li If a subfolder has no content (empty) then it is ignored. No hash created.
34 @li If the file is empty, it is also ignored / no hash created.
35 @li If the target directory (&inloc) is empty, &outds will also be empty
36
37 <h4> SAS Macros </h4>
38 @li mp_dirlist.sas
39
40 <h4> Related Files </h4>
41 @li mp_hashdataset.sas
42 @li mp_hashdirectory.test.sas
43 @li mp_md5.sas
44
45 @param [in] inloc Full filepath of the file to be hashed (unquoted)
46 @param [in] iftrue= (1=1) A condition under which the macro should be executed
47 @param [in] maxdepth= (0) Set to a positive integer to indicate the level of
48 subdirectory scan recursion - eg 3, to go `./3/levels/deep`. For unlimited
49 recursion, set to MAX.
50 @param [in] method= (MD5) the hashing method to use. Available options:
51 @li MD5
52 @li SH1
53 @li SHA256
54 @li SHA384
55 @li SHA512
56 @li CRC32
57 @param [out] outds= (work.mp_hashdirectory) The output dataset. Contains:
58 @li directory - the parent folder
59 @li file_hash - the hash output
60 @li hash_duration - how long the hash took (first hash always takes longer)
61 @li file_path - /full/path/to/each/file.ext
62 @li file_or_folder - "file", "folder", or "locked" for an entry that
63 is listed but cannot be opened at all (a broken link). The hash is
64 built from files, so a locked entry contributes no row
65 @li level - the depth of the directory (top level is 0)
66
67 @version 9.4m6
68 @author Allan Bowe
69**/
70
71%macro mp_hashdirectory(inloc,
72 outds=work.mp_hashdirectory,
73 method=MD5,
74 maxdepth=0,
75 iftrue=%str(1=1)
76)/*/STORE SOURCE*/;
77
78%local curlevel tempds maxlevel;
79
80%if not(%eval(%unquote(&iftrue))) %then %return;
81
82/* get the directory listing */
83%mp_dirlist(path=&inloc, outds=&outds, maxdepth=&maxdepth, showparent=YES)
84
85/* create the hashes */
86data &outds;
87 set &outds (rename=(filepath=file_path));
88 length FILE_HASH $32 HASH_DURATION 8;
89 keep directory file_hash hash_duration file_path file_or_folder level;
90
91 ts=datetime();
92 if file_or_folder='file' then do;
93 /* if file is empty, hashing_file will break - so ignore / delete */
94 length fname val $8;
95 drop fname val fid is_empty;
96 rc=filename(fname,file_path);
97 fid=fopen(fname);
98 if fid > 0 then do;
99 rc=fread(fid);
100 is_empty=fget(fid,val);
101 end;
102 rc=fclose(fid);
103 rc=filename(fname);
104 if is_empty ne 0 then delete;
105 else file_hash=hashing_file("&method",cats(file_path),0);
106 end;
107 hash_duration=datetime()-ts;
108run;
109
110proc sort data=&outds ;
111 by descending level directory file_path;
112run;
113
114%let maxlevel=0;
115data _null_;
116 set &outds;
117 call symputx('maxlevel',level,'l');
118 stop;
119run;
120
121/* now hash the hashes to populate folder hashes, starting from the bottom */
122%do curlevel=&maxlevel %to 0 %by -1;
123 data work._data_ (keep=directory file_hash);
124 set &outds;
125 where level=&curlevel;
126 by descending level directory file_path;
127 length str $32767 tmp_hash $32;
128 retain str tmp_hash ;
129 /* reset vars when starting a new directory */
130 if first.directory then do;
131 str='';
132 tmp_hash='';
133 i=0;
134 end;
135 /* hash each chunk of 100 file paths */
136 i+1;
137 str=cats(str,file_hash);
138 if mod(i,100)=0 or last.directory then do;
139 tmp_hash=hashing("&method",cats(tmp_hash,str));
140 str='';
141 end;
142 /* output the hash at directory level */
143 if last.directory then do;
144 file_hash=tmp_hash;
145 output;
146 end;
147 if last.level then stop;
148 run;
149 %let tempds=&syslast;
150 /* join the hash back into the main table */
151 proc sql undo_policy=none;
152 create table &outds as
153 select a.directory
154 ,coalesce(b.file_hash,a.file_hash) as file_hash
155 ,a.hash_duration
156 ,a.file_path
157 ,a.file_or_folder
158 ,a.level
159 from &outds a
160 left join &tempds b
161 on a.file_path=b.directory
162 order by level desc, directory, file_path;
163 drop table &tempds;
164%end;
165
166%mend mp_hashdirectory;