Can't find the information you are looking for here? Then leave a message over on our WinBatch Tech Support Forum.
;=================================================================================================== ; program: NDS Disk Space Ownership ; ; language: WinBatch ; extenders: NetwareX WWNWX34I.DLL ; ; author: Michael Harris ; (with many thanks to Detlev Dalitz for sample program at www.hpdd.de/hpdd/htm/dd390000.htm ; and to Steffen Fraas (sfraas@zoo.co.uk) for his SyntaxChecker) ; ; created: 4/23/2003 last updated: 6/19/2003 ; ; description: ; Program to obtain disk space ownership names and amounts from Novell Storage Services (NSS) volumes ; on which Netware is not set (by default, for performance reasons) to collect/maintain such information. ; ; NOTE: Offered as is. Use at your own risk. Not responsible for losses/damage arising from its use. ; Be sure to review it thoroughly, adjust it to your situation and test it before using it. ; ; NOTE: Designed to be run interactively or batch mode... but batch mode has not been tested. ; ; NOTE: Although can be run against a context (i.e. all volumes within a context), ; only keeps a single set of totals (needs to be enhanced to keep both volume specific ; and grand totals).... and would likely take a long time to run against multiple volumes. ; ; Traditional file system (TFS) volumes (Netware 3, 4), by default, collect/maintain ; volume level disk space ownership names and amounts info, which can apparently ; be quickly and conveniently obtained for reporting purposes via a single/few volume level Netware calls. ; Novell Storage Services (NSS) volumes (Netware 5, 6), by default, do not collect/maintain ; such info, for performance reasons. Our Novell consultant recommended against turning it on. ; ; This program is a brute force solution for occassionally obtaining such info for NSS volumes, ; by literally navigating volume(s) entire directory structures or specific subdirectory tree, ; querying and totaling file ownership and size for all files. Not pretty, and can take hours on large volumes ; (though does not appear to badly degrade the server or net, despite solid net traffic lights), ; but it works. ; ; The program uses an array (which is a persistent global variable), ; each row of which holds three items, to hold: ; - in row 0: program name, maximum usera allowed value, current user count ; (using row 0 for this help minimize the number of explicit variables that must be passed to functions) ; (this does, unfortunately, make the logic more difficult to read) ; - in rows 1 to maximumusers: ownername, total space used, total file count ; (row 1 is preloaded with a fictitious ".(None).(None)" ownername, for optimization purposes ; - in row maximumusers+1: previous ownername encountered, previous owner's position (index) in the array ; (this maximumusers+1 row is used for optimization purposes ; and helps minimize the number of explicit variables that must be passed to functions) ; (this does, unfortunately, make the logic more difficult to read) ; ; Program logic overview: ; - housekeeping and array allocation ; - array initialization ; - causes the WinBatch interpreter to scan all the user defined functions, ; which allows the user defined functions to be placed below the main logic ; - queries the user for a target context (can do multiple volumes, but not recommended), ; volume or subdirectory within a specific volume ; - obtains a list of volume(s) from NDS that satisfy the user specification ; - sorts the volume list ; - starts the output file (writes date/time stamped report header lines) ; - initializes array (see discussion above as to contents/use of array) ; (this includes use of rows 0, 1 and maximumusers+1 for special/optimization purposes) ; - for each volume in the volume list ; - concatenate the NDS tree name and the volumepath (and ":\" root if processing entire volume) ; - map a drive letter to the volume root or volume:\directorypath ; - if mapping fails, continue with next volume (next iteration of the per volume processing) ; - recursively descend/process the drive letter's directory tree ; - processing files in each directory ; (when a new file owner is encountered, a row for the new is added to the array) ; (files with no owner are totaled for fictitious ".(None).(None)" owner) ; (optimization used.... ".(None).(None)" placed in row 1 of array to speed hitting it ; and previous ownername and location (index) in array maintained in row maximumuser+1 ; for use in quickly testing/utilizing that information when current file owner ; is same as previous file's owner) ; - recursively descending into/processing subdirectories in each directory ; - sort and write the user totals out in ownercontext ownername sequence, ; obtaining and adding each owner's first and last name ; - at program user's option, also import the tab delimited output file into Excel ; and save it out as an Excel spreadsheet (currently accomplished via feeding keystrokes ; to Excel... but there is a better way to do it... via OLE/ODBC/.... WinBatch extension. ; ; NOTE: The program uses "typeless" NDS path names ; and reverses them for human readability sake. ; ; NOTE: Reporting of inability to descend into a (corrupt?/illegally named?) directory ; should be improved. Currently, program reports the problem and waits for user ; to click OK before skipping that bad directory and proceeding. ; Should have it quietly generate a report line (in row(s) at tail end of array or elsewhere) ; for eventual inclusion in the report.... so that errors appear in report ; but do not pause program execution until user oks/resumes processing. ; ; ; ; possible enhancements/optimizations to be pursued: ; - rename/prefix all user defined functions whose names don't start with "udf" so that they do ; - report starting/ending/elapsed time ; - per volume totals plus grand totals... (or else remove/discourage ability to process multiple volumes, ; due to excessive time that might take?) ; - use of additional "standard/housekeeping" array for passing dialogtitle, etc. easily/concisely ; to all functions (or is there such as thing as globals that don't have to be passed?) ; (actually, DialogTitle is stored in UserArray[0,0] in this program.... but should probably ; develop a standard array/arrayname for this or find workaround) ; (any way to avoid need to explicitely reference arrays (globals) when calling functions?) ; - restart/resume capability?, to help deal with long run times on NSS volumes? ; (would require some sort of "recursive find but don't process" to find starting point, ; then turn on process from that point forward) ; - or when do search loop, loop backward from latest to first.... as latest is most likely? ; (keep in mind that special case optimizations "(None)" and "last/most recent" are already implemented) ; (and/or load the array with all user accounts from NDS (with zeros counters), plus necessary pseudo accounts, ; in sequence, and then employ binary sort for faster hits) ; - some alternative to typeless variables/array entries.... to reduce type determination/conversion ; processing when know will be string or number? (binary buffers?) ; - turn errormode off and on selectively, rather than setting/leaving off throughout program? ; - for stabler/safer import of tab delimited output file into Excel, ; investigate use of other WinBatch extender (OLE/ODBC/etc.) rather than feeding keystrokes to Excel ; .... and consider whether to do additional formatting, column totals, etc. ; (original thought was to keep output plain/minimalist... for possible use by other programs ; and to let those viewing reports in Excel rework them as they saw fit) ; - consider whether is worth retaining the delimited text file once it has been imported into Excel.... ; if not, could delete it (so as to not clutter up the reports directory ; and confuse users with choice between identically named (except for file type) text and Excel files. ; ; ; ; output files location: ; Currently written to the following location, for group/shared use: ; \R006_server\V006\groups\Isd_techservices\WinBatch\Reports ; with a file name: ; "NDS Disk Space Ownership On %CurrentTimeYmdHmsWithBlanks%.txt ;=================================================================================================== ;============================ SET DEFAULT VALUES AND ENVIRONMENT HERE ============================== ; default Values NDSTree = '\\ACME_TREE\' UNC = '.ACME' ; command line argument #1 (param1) ReportPath = 'F:\Winbatch\' ; location for reports during testing ;ReportPath = '\\R006_server\V006\groups\Isd_techservices\WinBatch\Reports\' DialogTitle = 'NDS Disk Space Ownership' MappedDrive = "L:" SpreadsheetDelimiter = @TAB MaximumUsers = 2000 ; max number of file owners (users) (estimate high!) ; (should pad/estimate high) ; (owner "" returned if owner no longer exists) ; NOTE: Arrays (such as following one) are global, ; i.e. their address is passed rather than their contents ; and their contents persist across function calls/returns ; They apparently must be explicitely passed to functions, ; but do not have to be explicitely returned from functions. UserArray = ArrDimension((MaximumUsers + 2), 3) ; allocate the user/owner array, with three ; items (username, spaceused, filecount) per user ; NOTE: First (0th) row in array is used to pass info between functions ; UserArray[0,0] is used for DialogTitle text ; UserArray[0,1] is used for MaximumUsers value ; UserArray[0,2] is used for UserCount value ; Second (1st) through MaximumUsers row is used to store owners encountered ; (note: fictious ".(None).(None)" user will be pre-loaded into second (1st) row ; for optimization purposes... and usercount value in first (0th) row set ; will be set accordingly) ; Last (MaximumUsers+1) row is used to keep track of most recent owner dealt with OutputFileNameArray = ArrDimension(1) ; used to return/pass output/report file's name ;--------------------------------------------------------------------------------------------------- ; error handling ErrorMode(@OFF) ; tolerate errors, necessary for NDS processing ; generic file system related IntControl(5, 1, 0, 0, 0) ; process system and hidden directories and files ; Netware/NDS related AddExtender('WWNWX34I.DLL') ; load NetwareX extender (Netware/NDS API) nwSetOptions(1, @TRUE) ; tell NetwareX to return typless names from NDS ; make user defined functions (UDFs below) defined/known to WinBatch interpreter ; (otherwise, must place them here, ahead of the code that calls/invokes them) Gosub DefineUDFs ; interpreter scans UDFs then returns here ; ; so that UDFs can be placed below main code ;=================================================================================================== ; main program logic starts here ; open box and report program start MessageLine = "Running..." BoxOpen(DialogTitle, MessageLine) ; assure valid number of command line arguments (0 for interactive, 1 for batch/command line) If ((param0 < 0) || (param0 > 1)) ; invalid number, report and terminate the program MessageLine = StrCat("ERROR: Invalid number of command line arguments = ", param0) Pause(DialogTitle, MessageLine) Exit Endif ; get valid base object specification for NDS search filter from user or command line ; (NDS tree plus UNC (.context, .volume.context, or .volume.context:filesystempath)) BaseObjectSpec = udfGetValidUNC(NDSTree, UNC, DialogTitle, param0) ; NOTE: at this point BaseObjectSpec has the tree name prefixed on it ; get list of volume(s) within/that satisfy base object specification ; one (if filesystem path was supplied) or many If (ItemCount(BaseObjectSpec, ":") == 2) ; two items in NDSTreeUNCEntered, separated by ":", means file system path was supplied ; which means some portion of the file system of only one volume will be processed ; so store combined .volume.context:filesystempath as single item in volume list ; (note that later ItemCount of this, using @TAB as delimiter, will correctly return count of 1 item) VolumeList = BaseObjectSpec ; but remove the NDS tree name prefix, to facilitate sorting (we'll re-prefix later when mapping drive) VolumeList = StrReplace(VolumeList, NDSTree, "") Else ; only one item was found in NDSTreeUNCEntered, which means no file system path ; was supplied..... which means one or more entire volumes will be processed ; so use what was entered as base object specification for search of NDS for volumes ; (which may return one or more volumes) ; (volume object name(s) in list will be typless, distinguished, tab delimited, unsorted) ; get the list of volumes VolumeList = udfGetVolumeList(BaseObjectSpec) ; (note that volume names in list do not have NDS tree name prefix.... ; as are not returned by nwSearchObjects...... ; we'll re-prefix later when mapping drives) ; sort the volume list VolumeList = udfSortNDSObjectList(VolumeList) Endif ; diagnostic, if needed ;DiagnosticLine = StrCat("VolumeList = ", VolumeList) ;Message(DialogTitle, DiagnosticLine) ; create, open and write header lines/records to the output file ; (doing this here, to protect against processing volume(s), only to find couldn't output file) ReportFileHandle = udfStartOutputFile(ReportPath, DialogTitle, BaseObjectSpec, SpreadsheetDelimiter, OutputFileNameArray) ; diagnostic, if needed ;DiagnosticLine = StrCat("After udfStartOutputFile... ReportPath = ", ReportPath, " ReportFileHandle = ", ReportFileHandle) ;Message(DialogTitle, DiagnosticLine) ; process all the volumes specified/found ; determine number of volume objects returned (contained in VolumeList) NumberOfVolumes = ItemCount(VolumeList, @TAB) ; if running interactively, report number of volumes specified or found in the search scope If (param0 == 0) MessageLine = StrCat("Number of volumes specified/found = ", NumberOfVolumes, @CR) MessageLine = StrCat(MessageLine, "Please wait while file is produced....") BoxText(MessageLine) Endif ; ; initialize user array (for each user, contains username, spaceused, filecount) ; (user array is a global (address, rather than contents get passed, so persists across function calls) ; initialize user array header ; (row 0, special case used to pass program name, maximumusers and current user count between functions) UserArray[0,0] = DialogTitle ; program name/dialog title for use in displaying messages UserArray[0,1] = MaximumUsers ; maximum number of users/owners (and number of user rows in array) UserArray[0,2] = 0 ; current number of users/owners encountered, start with count of 0 ; ; initialize main part of array (row 1 to maximumusers, where users/owners encountered are stored and summed) ; (may not be necessary for single report.... but keep to be safe.... and to have if enhance to do per volume reports) For UserIndex = 1 to (MaximumUsers) by 1 UserArray[UserIndex,0] = "" ; empty username UserArray[UserIndex,1] = 0.0 ; zero spaceused (floating point, to handle large amounts) UserArray[UserIndex,2] = 0 ; zero filecount Next ; ; as a significant number of our volumes at our installation have many files that have no owners ; (due to having been mass copied from elsewhere), optimize/speed things a bit ; by preloading the pseudo account ".(None).(None)" as the first owner (row 1), ; so that the for loop based search (or other code that knows its location) will always ; encounter it first, rather than looping through all previously encountered owners UserArray[0,2] = 1 ; adjust array header (first (0th) row) usercount to 1 to reflect this) UserArray[1,0] = ".(None).(None)" ; add pseudo account ".(None).(None)" as first owner entry in array (second (1st) row) UserArray[1,1] = 0.0 ; set its spaceused total to 0 UserArray[1,2] = 0 ; set its files count to 0 ; ; as any given file will often prove to be owned by the owner of the previously encountered file, ; another optimization is used down in udfAddToUserArray.... ; if the current file's owner is same as the previous file's owner, uses saved previous owner's ; array index to directly address and add the current file's size to and increment the file count of ; that previous owner, rather than using a for loop to sequentially search the array for the previous owner. ; To make this work, need to initialize the "previous file owner name & index" trailer record/row (row maximumusers+1) ; at the end of the array to a "name of fictitious owner who could never have possibly been encountered before" here ; (should not be "", as files without owners return that) UserArray[(MaximumUsers + 1), 0] = "Fictitious Previous Owner Name" ; loop through and process each volume ; (any per volume initialization/totals/etc. needed? ... would be if were to enhance to do per volume reports) For VolumeListIndex = 1 to NumberOfVolumes by 1 ; process current volume ; extract current volume or volume/path from tab delimited list VolumePath = ItemExtract(VolumeListIndex, VolumeList, @TAB) ; prefix it with the NDS Tree name, ; as nwSearchObjects doesn't return it as part of names of found objects (volumes) VolumePath = StrCat(NDSTree, VolumePath) ; assure that ":\filesystempath" is present (add ":\" root file system path if not, i.e. if processing entire volume) If (ItemCount(BaseObjectSpec, ":") == 1) ; no file system path present, so add ":\" VolumePath = StrCat(VolumePath, ":\") Endif ; map root mapped drive letter to volume:\filesystempath directory of volume nwMap(VolumePath, MappedDrive, 0) ReturnCode = LastError() If (ReturnCode != 0) ; map was not successful, report it ; ? need to enhance this to report it via line in output report, ; for batch or unattended use... so won't pause/hang a run if map attempt fails? DiagnosticLine = StrCat("Was unable to map VolumePath: ", VolumePath, " ReturnCode = ", ReturnCode) Message(DialogTitle, DiagnosticLine) ; skip to beginning of loop (i.e. skip this volume) Continue Endif ; report volume processing start if running interactively If (param0 == 0) ; report that are processing current volume ProgressLine = StrCat(MessageLine, @CR, "Processing volumepath: ", VolumePath) BoxText(ProgressLine) Endif ; diagnostic, if needed ;DiagnosticLine = StrCat("Preparing to process folder tree for MappedDrive ", MappedDrive) ;Message(DialogTitle, DiagnosticLine) ; process directories/folders for current volume or volumepath udfProcessFolderTree(MappedDrive, UserArray) ; any per volume totals, etc.???? Next ; all volumes/directories/files have been processed ; write the user totals out udfWriteOutputFile(ReportFileHandle, DialogTitle, SpreadsheetDelimiter, UserArray) ; close the output file FileClose(ReportFileHandle) ; if running interactively, close "please wait" box and provide ending message to user If (param0 == 0) BoxShut() MessageLine = StrCat("DONE.", @CR) MessageLine = StrCat(MessageLine, "See delimited text file:", @CR, OutputFileNameArray[0]) Message(DialogTitle, MessageLine) Endif ; if running interactively, offer the user the option of also saving the file in spreadsheet file format If (param0 == 0) ; ask user if wish to also save the file as a spreadsheet file ; change output file name file type from .txt to .xls SpreadsheetFileName = ItemReplace("xls", ItemCount(OutputFileNameArray[0], "."), OutputFileNameArray[0], ".") MessageLine = "Also save as spreadsheet file (same name, with .xls file type)?" MessageLine = StrCat(MessageLine, @CR, SpreadsheetFileName) YesNoAnswer = AskYesNo(DialogTitle, MessageLine) If (YesNoAnswer == @YES) ; save the file out as a spreadsheet by importing it into Excel SaveFileAsSpreadsheet(OutputFileNameArray, SpreadsheetFileName, DialogTitle) Endif Endif ; end program Exit ; main program logic end here ;============================ DEFINE USER DEFINED FUNCTIONS (UDFs) ================================= :DefineUDFs ; label for interpreter's gosub fallthru of UDFs ; ; so that UDFs can be placed below main code ; ; (interpreter must apparently scan user defined ; ; functions before they can be called..... doing this ; ; would not be necessary if user defined functions ; ; were placed ahead of/above main program logic ; ; as interpreter would then do a fall-thru scan ; ; before hitting and executing the main logic ;=================================================================================================== ; note: Function variables are local, subroutine variables are global. ; The user array allocated at the top of the program is global (address is passed, not contents) ; (must explicitely pass it, but don't have to return it.... contents persist across functions). ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfGetValidUNC(NDSTree, UNC, DialogTitle, param0) ; get valid base object specification for NDS search filter ; (NDS tree plus user supplied UNC (.context, .volume.context, or .volume.context:filesystempath)) ; being run interactively or batch/command line ? If (param0 == 0) ; interactively (no command line arguments were supplied) UNCEntered = "" NDSTreeUNCEntered = "" ResultCode = 1 ; loop till get valid base object specification from user While ((ResultCode != 0) || (IsNumber(UNCEntered)) || (StrSub(UNCEntered, 1, 1) != ".")) MessageLine = "NDS .context, .volume.context or .volume.context:filesystempath " MessageLine = StrCat(MessageLine, "within ", NDSTree, " ?") MessageLine = StrCat(MessageLine, @CR, "CAUTION: Multiple volumes will be slow.") UNCEntered = AskLine(DialogTitle, MessageLine, UNC) ; concatenate NDS tree plus user supplied UNC (.context, .volume.context, or .volume.context:filesystempath) NDSTreeUNCEntered = StrCat(NDSTree, UNCEntered) ; verify that .context or .volume.context portion of UNC supplied exists If (ItemCount(NDSTreeUNCEntered, ":") == 1) ; only one item, no optional ":filesystempath" present in UNC supplied ; verify that it exists? nwGetObjInfo(NDSTreeUNCEntered, 2) ResultCode = LastError() Else ; optional ":filesystempath" present in UNC supplied ; verify that it (full .volume.context:filesystempath) exists nwGetDirInfo(NDSTreeUNCEntered, 0, "", "", 0) ResultCode = LastError() Endif If ((ResultCode != 0) || (IsNumber(UNCEntered)) || (StrSub(UNCEntered, 1, 1) != ".")) MessageLine = "ERROR: NDS .context, .volume.context or .volume.context:filesystempath not found." MessageLine = StrCat(MessageLine, "You entered = ", UNCEntered) Message(DialogTitle, MessageLine) Endif Endwhile Else ; batch/command line execution (not interactive) ; UNC (.context, .volume.context or .volume.context:filesystempath) supplied as first command line argument UNCEntered = param1 ; concatenate NDS tree plus supplied UNC (.context, .volume.context, or .volume.context:filesystempath) NDSTreeUNCEntered = StrCat(NDSTree, UNCEntered) ; verify that .context or .volume.context portion of UNC supplied exists If (ItemCount(NDSTreeUNCEntered, ":") == 1) ; only one item, no optional ":filesystempath" present in UNC supplied ; exists as an NDS object? nwGetObjInfo(NDSTreeUNCEntered, 2) ResultCode = LastError() Else ; optional ":filesystempath" present in UNC supplied ; full .volume.context:filesystempath exists? nwGetDirInfo(NDSTreeUNCEntered, 0, "", "", 0) ResultCode = LastError() Endif If ((ResultCode != 0) || (IsNumber(UNCEntered)) || (StrSub(UNCEntered, 1, 1) != ".")) ; for batch/command line run, should error message go to console or to report file? ; for now, display it on the console MessageLine = "ERROR: NDS .context, .volume.context or .volume.context:filesystempath " MessageLine = StrCat(MessageLine, NDSTreeUNCEntered, " not found! Abending!") Message(DialogTitle, MessageLine) ; end the program Exit Endif Endif ; return valid base object specification for NDS search Return (NDSTreeUNCEntered) #EndFunction ; ;---------------------------------------------------------------------------------------------------- ; #DefineFunction udfGetVolumeList(BaseObjectSpec) ; get (unsorted) list from NDS of volume objects that satisfy the base object specification ; set up a search filter ; (is cumulative... use *FREE_BUFFER* to reset/deactivate) ; (source of attribute names was the NDS Snoop program.... ; see www.novell.com/coolsolutions/tools/1005.html) ; (another source of attribute names is the output displayed by the example ; WinBatch program supplied in the nwGetObjValue writeup in the NetwareX help) nwSearchFilter(BaseObjectSpec, 'BASECLS','',0,0,0) nwSearchFilter(BaseObjectSpec, 'ANAME','Volume',0,0,0) nwSearchFilter(BaseObjectSpec, 'END','',0,0,0) ; set up search flags (combine/add various bit flag values) ; value of 2 says search scope is base-object and all its subordinates Flags = 2 ; tell WinBatch's NetwareX extensions to return typeless names from NDS nwSetOptions(1,@TRUE) ; do the search and get the result code VolumeListDistinguishedTabbed = nwSearchObjects(BaseObjectSpec, '', '', Flags, '') ResultCode = LastError() ; verify that search was ok If (ResultCode != 0) ; diagnostic (if needed) report result code MessageLine = StrCat("Volumes search failed with result code = ", ResultCode) ;Message(DialogTitle, MessageLine) Message("NDS Disk Space Ownership", MessageLine) ; End the program Exit Endif ; reset/deactivate the search filter (don't need when done with nwSearchObjects) nwSearchFilter('', '*FREE_BUFFER*', '', 0, 0, 0) ; diagnostic (if needed) ;Message("NDS Disk Space Ownership", VolumeListDistinguishedTabbed) ; diagnostic program termination (if needed) ;exit ; return list of volume objects found Return (VolumeListDistinguishedTabbed) #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfSortNDSObjectList(NDSObjectList) ; Sort the list (comes from NDS in unsorted sequence) by ; a) flipping the NDS names from ".object.container..org" to ".org.container..object", TempNDSObjectList = NDSObjectList TempNDSObjectList = PadAndReverseNDSNames(TempNDSObjectList) ; b) sorting the list of flipped names (into organizational chart sequence) TempNDSObjectList = ItemSort(TempNDSObjectList, @TAB) ; diagnostic (if needed) ; Message(DialogTitle, UserListDistinguishedTabbed) ; c) reflipping the names back to ".object.container..org" TempNDSObjectList = UnpadAndReverseNDSNames(TempNDSObjectList) ; diagnostic (if needed) ;Message(DialogTitle, ObjectListDistinguishedTabbed) ; diagnostic program termination (if needed) ;exit ;return sorted list of NDS objects Return (TempNDSObjectList) #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction PadAndReverseNDSName(NDSName) ; Inserts a blank (space) name segment (' ') between object name segment and remaining path segments ; (so that objects will sort out ahead of subcontainers) (empty segment will be removed later) ; and reverses all segments of each (now padded) NDS name in list ; from ".object..container...org" to ".org...container..object" ; The result is useful for sorting into organizational chart order.... ; but must then be unpadded and reversed again before can be used to query NDS objects. NDSName = ItemInsert(' ', 2, NDSName, '.') ReversedNDSName = NDSName NumberOfSegments = ItemCount(NDSName, '.') For SegmentIndex = 1 to NumberOfSegments by 1 Segment = ItemExtract(SegmentIndex, NDSName, '.') ReversedNDSName = ItemReplace(Segment, (NumberOfSegments - SegmentIndex + 1), ReversedNDSName, '.') Next Return (ReversedNDSName) #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction PadAndReverseNDSNames(ListOfNDSNames) ; Inserts a blank (space) name segment (' ') between object name segment and remaining path segments ; (so that objects will sort out ahead of subcontainers) (empty segment will be removed later) ; and reverses all segments of each (now padded) NDS name in list ; from ".object..container...org" to ".org...container..object" ; The result is useful for sorting into organizational chart order.... ; but must then be unpadded and reversed again before can be used to query NDS objects. NumberOfNames = ItemCount(ListOfNDSNames, @TAB) ; Process names, a name at a time ListofReversedNDSNames = ListofNDSNames For ListIndex = 1 to NumberOfNames by 1 NDSName = ItemExtract(ListIndex, ListOfReversedNDSNames, @TAB) NDSName = ItemInsert(' ', 2, NDSName, '.') ReversedNDSName = NDSName NumberOfSegments = ItemCount(NDSName, '.') For SegmentIndex = 1 to NumberOfSegments by 1 Segment = ItemExtract(SegmentIndex, NDSName, '.') ReversedNDSName = ItemReplace(Segment, (NumberOfSegments - SegmentIndex + 1), ReversedNDSName, '.') Next ListOfReversedNDSNames = ItemReplace(ReversedNDSName, ListIndex, ListOfReversedNDSNames, @TAB) Next Return (ListOfReversedNDSNames) #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction UnpadAndReverseNDSNames(ListOfReversedNDSNames) ; Reverses all segments of each (now padded) reversed NDS name in list ; from ".org...container..object" back to ".object..container...org" ; and removes the blank (space) name segment (' ') from between object name segment and remaining ; path segments (was there so that objects would sort out ahead of subcontainers). ; The result is a list of object names, in organizational chart order, but with names ; in regular ".object..container...org" NDS notation, so can be used to call NDS for object info. NumberOfNames = ItemCount(ListOfReversedNDSNames, @TAB) ; Process names, a name at a time ListofNDSNames = ListOfReversedNDSNames For ListIndex = 1 to NumberOfNames by 1 ReversedNDSName = ItemExtract(ListIndex, ListOfNDSNames, @TAB) NDSName = ReversedNDSName NumberOfSegments = ItemCount(ReversedNDSName, '.') For SegmentIndex = 1 to NumberOfSegments by 1 Segment = ItemExtract(SegmentIndex, ReversedNDSName, '.') NDSName = ItemReplace(Segment, (NumberOfSegments - SegmentIndex + 1), NDSName, '.') Next NDSName = ItemRemove(3, NDSName, '.') ListOfNDSNames = ItemReplace(NDSName, ListIndex, ListOfNDSNames, @TAB) Next Return (ListOfNDSNames) #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfStartOutputFile(ReportPath, DialogTitle, BaseObjectSpec, SpreadsheetDelimiter, OutputFileNameArray) ; create a meaningfully named date/time stamped output file, open it, write header lines/records ; and return its file handle number ; get current date/time CurrentTimeYmdHms = TimeYmdHms() ; semi-reverse the baseobjectspec from ; //treename/.volume...context:filesyspath to //treename/.context...volume:filesyspath ; for human oriented sortability and readability in file name and report header TreeName = ItemExtract(1, BaseObjectSpec, ".") VolumeToContextPath = StrReplace(BaseObjectSpec, TreeName, "") VolumeToContextPath = ItemRemove(1, VolumeToContextPath, ".") VolumeToContextPath = ItemExtract(1, VolumeToContextPath, ":") ItmCnt = ItemCount(VolumeToContextPath, ".") ContextToVolumePath = VolumeToContextPath For Index = 1 to ItmCnt by 1 Item = ItemExtract(Index, VolumeToContextPath, ".") ContextToVolumePath = ItemReplace(Item, (ItmCnt - Index + 1), ContextToVolumePath, ".") Next If (StrScan(BaseObjectSpec, ":", 1, @FWDSCAN) == 0) FileSysPath = "" Else FileSysPath = ItemExtract(-1, BaseObjectSpec, ":") Endif SemiReversedBaseObjectSpec = StrCat(TreeName, ContextToVolumePath, " ", FileSysPath) ; generate output file name and open file (will go where ReportPath (see top of program) variable specifies) ; replace periods and optional colon and slashes in baseobjectspec with blanks SemiReversedBaseObjectSpec = StrReplace(SemiReversedBaseObjectSpec, "\\", "") SemiReversedBaseObjectSpec = StrReplace(SemiReversedBaseObjectSpec, "\", " ") SemiReversedBaseObjectSpec = StrReplace(SemiReversedBaseObjectSpec, ".", " ") ; replace colons in time with blanks CurrentTimeYmdHmsWithBlanks = StrReplace(CurrentTimeYmdHms, ':', ' ') OutputFileName = ReportPath OutputFileName = StrCat(OutputFileName, DialogTitle, " For ", SemiReversedBaseObjectSpec, "On ") OutputFileName = StrCat(OutputFileName, CurrentTimeYmdHmsWithBlanks, '.txt') ; to be safe, delete file by that name if already exists (not likely, but best to be safe ; in case change file naming strategy (though "WRITE" should zero out and overwrite existing file) If (FileExist(OutputFileName)) FileDelete(OutputFileName) Endif ; create/open the file ReportFileHandle = FileOpen(OutputFileName, 'WRITE') ; successful create/open of file ? If (ReportFileHandle == 0) ; create/open was not successful MessageLine = StrCat('File open/create of ', OutputFileName, ' failed') Message(DialogTitle, MessageLine) ;terminate the program Exit Endif ; Write a report header record (with title/info spread across several columns, ; so as to not mess up the autoadjusted width of columns when this ; file is imported into Excel via Data | Get External Data | Import Text File. ; (note that width of title (including trailing spaces) causes Excel to ; adjust column widths accordingly during data import of this file) ;xxxxOutputFileName = StrCat(OutputFileName, 'NDS Users Home Directory Space Limits And Usage On ') ;xxxxOutputFileName = StrCat(OutputFileName, CurrentTimeYmdHmsWithBlanks, '.txt') OutputLine = "" OutputLine = StrCat(OutputLine, DialogTitle, " for ", SemiReversedBaseObjectSpec, SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "(produced: ", CurrentTimeYmdHms, ")") ;a CR-LF is appended at the end of the line by the write FileWrite(ReportFileHandle, OutputLine) ; Write a blank header record to separate header title from column heading line OutputLine = "" FileWrite(ReportFileHandle, OutputLine) ; Write a header record with field descriptions delimited by tabs ; (note that width of title (including trailing spaces) causes Excel to ; adjust column widths accordingly during data import of this file, ; but that later detail (user) records having even wider fields will further widen them)) OutputLine = "" OutputLine = StrCat(OutputLine, "NDS Path (reversed) ", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "User ", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "Last Name ", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "First Name ", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "Space Used (MB) ", SpreadsheetDelimiter) OutputLine = StrCat(OutputLine, "File Count") ;a CR-LF is appended at the end of the line by the write FileWrite(ReportFileHandle, OutputLine) ; Write a blank header record to separate header from user lines/records OutputLine = "" FileWrite(ReportFileHandle, OutputLine) ; pass output file name back, via global array, for later display in message to user ; to facilitate user knowing the (automatically generated) name of the report file OutputFileNameArray[0] = OutputFileName Return (ReportFileHandle) #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfWriteOutputFile(ReportFileHandle, DialogTitle, SpreadsheetDelimiter, UserArray) ; copy the user array into a list.... investigate array sorts later) ; reverse/pad user NDS names to .org.container.. .username, for sortability (and readability) ; precompute MegaByte constant value MegaByte = 1024 * 1024 ; diagnostic, if needed ; show the users records/rows in the array ;For UserIndex = 0 to UserArray[0,2] ; MessageLine = StrCat(UserArray[UserIndex,0], " ", UserArray[UserIndex,1], " ", UserArray[UserIndex,2], @CR) ; Message(DialogTitle, MessageLine) ;Next ; convert user (owner names and space totals) array to single cumulative user list (containing tab delimited strings) ; for readability and performance (?) reasons, store current user count from user array to meaningfully spelled ; non-array variable UserCount = UserArray[0,2] ; initialize the cumulative user list to be empty UserList = "" ; extract user array rows and accumulate/append them into the cumulative user list For UserIndex = 1 to UserCount by 1 ; Extract User simple name from full typeless name (period delimited, with leading period) UserFullTypelessName = UserArray[UserIndex,0] User = ItemExtract(2, UserFullTypelessName, ".") ; Generate a reversed (root-to-container) version of user object's NDS path ; Remove leading period (empty item and its period delimiter, in terms of period delimited list) NDSPath = ItemRemove(1, UserFullTypelessName, ".") ; Remove User simple name item (and its period delimeter) NDSPath = ItemRemove(1, NDSPath, ".") ; Determine number items (number of NDS path segments) remaining ItmCnt = ItemCount(NDSPath, ".") ; Swap/reverse the remaining items (path segments) ; Initialize reversed name with proper number of items and delimiters by brute force cheat ; of copying the "to be reversed" list into it ReversedNDSPath = NDSPath ; now swap/reverse the segments of the name For Index = 1 to ItmCnt by 1 ; This could be done as a single line, but is broken out for readability and troubleshooting Item = ItemExtract(Index, NDSPath, ".") ReversedNDSPath = ItemReplace(Item, (ItmCnt - Index + 1), ReversedNDSPath, ".") Next ; Get user's Surname (last name) from NDS Surname = nwGetObjValue(UserFullTypelessName, 'Surname', "", "", 1) ; Protect against/flag (as "(None)") empty name (i.e. if returned a 0) If (Surname == 0) Surname = '(None)' Endif ; Get user's Given Name (first name) from NDS GivenName = nwGetObjValue(UserFullTypelessName, 'Given Name', "", "", 1) If (GivenName == 0) GivenName = '(None)' Endif ; Get user's total space used from the user array SpaceUsedInMB = Int((UserArray[UserIndex,1] / MegaByte) + 0.5) ; construct the user's "record", consisting of the above fields, delimited by tabs UserRecord = StrCat(ReversedNDSPath, @TAB, User, @TAB, Surname, @TAB, GivenName, @TAB, SpaceUsedInMB, @TAB, UserArray[UserIndex,2], @CR) ; concatenate/append the user's "record" to the end of the cumulative user list UserList = StrCat(UserList, UserRecord) Next ; diagnostic, if needed ;DiagTitle = StrCat("unsorted user list") ;Message(DiagTitle, UserList) ; sort the user list UserList = ItemSort(UserList, @CR) ; diagnostic, if needed ;DiagTitle = StrCat("sorted user list") ;Message(DiagTitle, UserList) ; remove what is now an empty first item/record from the list (artifact of last user entry ending with @CR delimiter) UserList = ItemRemove(1, UserList, @CR) ; diagnostic, if needed ;DiagTitle = StrCat("sorted user list minus empty first item/record") ;Message(DiagTitle, UserList) ; write out the per user file space usage and file count For UserIndex = 1 to UserCount by 1 ; extract current user's fields from user list and build output line from them OutputLine = ItemExtract(UserIndex, UserList, @CR) ; write the line out to the file ; (a CR-LF is appended at the end of the line by the write) FileWrite(ReportFileHandle, OutputLine) Next Return #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfProcessFolderTree(sFolder, UserArray) ; WARNING: This function employs a recursive call of itself to accomplish recursive descent of directories ; save current drive/folder location sCurrentFolder = DirGet() ; change current drive/directory to supplied location ; For first invocation, will be to volume root or subdirectory specified by user ; For successive (recursive) self-invocations, will be to lower level subdirectories DirChange(sFolder) ; did directory change work? (i.e. detect/report/skip corrupted subdirectory ; (how should this ultimately be handled..... add error line to report instead of pausing/reporting ; to the user via the console?) ReturnCode = LastError() If (ReturnCode != 0) ; directory change was not successful, report it DiagnosticLine = StrCat("At ", sCurrentFolder, " Unsuccessful directory change to ", sFolder) Message("udfProcessFolderTree", DiagnosticLine) Else ; diagnostic, if needed ;DiagnosticLine = StrCat("Preparing to process folder sFolder = ", sFolder) ;Display(1, "udfProcessFolderTree", DiagnosticLine) ;Message("udfProcessFolderTree", DiagnosticLine) ; process current directory's files ; (remember, userarray is global... must be explicitely passed (address is passed, not contents), ; but doesn't have to be explicitely returned) ; process current directory's files udfProcessFolderFiles(sFolder, UserArray) ; get list and count of subdirectories within current directory ; (need to error check?) sFolderList = DirItemize("*.*") iFolderCount = ItemCount(sFolderList, @TAB) ; process current directory's subdirectories For FolderIndex = 1 to iFolderCount ; extract current subdirectory from folder list sThisFolder = ItemExtract(FolderIndex, sFolderList, @TAB) ; recursively descend into/process current subdirectory udfProcessFolderTree(sThisFolder, UserArray) ;RECURSIVE Next Endif ; recursively ascend DirChange(sCurrentFolder) Return #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfProcessFolderFiles(sFolder, UserArray) ; get list and count of files in current directory ; (need to error check fileitemize?) sFileList = FileItemize("*.*") iFileCount = ItemCount(sFileList, @TAB) ; diagnostic, if needed ; DiagnosticLine = StrCat("sFolder = ", sFolder, "Preparing to process files sFileList = ", sFileList, " iFileCount = ", iFileCount) ; Message(UserArray[0,0], DiagnosticLine) For FileIndex = 1 to iFileCount by 1 ; extract current file name from file list sThisFile = ItemExtract(FileIndex, sFileList, @TAB) ; as are using NetwareX function, need full drivepathfilename, so obtain it sThisDriveFile = FileFullname(sThisFile) ; diagnostic, if needed ;DiagnosticLine = StrCat("sFolder = ", sFolder, " sThisFile = ", sThisFile, " sThisDriveFile = ", sThisDriveFile) ;Message(UserArray[0,0], DiagnosticLine) ; process current file udfProcessFile(sThisDriveFile, UserArray) Next Return #EndFunction ; ;--------------------------------------------------------------------------------------------------- ; #DefineFunction udfProcessFile(sDriveFileName, UserArray) ; get file's owner's user object name (will be in .user.context..org format) ; (note: Files whose owners no longer exist or whose owner info was lost ; during a mass copy (example: server migration) will have empty ("") owners.... ; It is also possible for odd (non-NDS?) owner names/values to be present, ; such as 060B0398201B3C4NPI398201_P3+30c, which returns "" as though empty) OwnerObjectName = nwGetFileInfo(sDriveFileName, 2, "", "", 0) ; diagnostic, if needed ;DiagnosticLine = StrCat("OwnerObjectName = X", OwnerObjectName, "X ResultCode = ", ResultCode) ;Message("udfProcessFile", DiagnosticLine) ; Note: an empty owner (example: owner account has been deleted) ; comes back as an empty string and a ResultCode of 0 ; protect against possibility that file no longer exists (i.e. was deleted/renamed during time of this run) ResultCode = LastError() If (ResultCode == 0) ; file exists ; if ownerobjectname is empty (""), owner no longer exists, so substitute ".(None).(None)" for context and name If (OwnerObjectName == "") ; substitute OwnerObjectName = ".(None).(None)" Else ; if ownerobjectname is Netware "[Supervisor]" (old NW3/4 bindery term?), substitute ".[Supervisor].(None)" for context ; (or need to generalize this for all "only contains one period/segment" ownerobjectnames?) If (OwnerObjectName == ".[Supervisor]") ; substitute OwnerObjectName = ".[Supervisor].(None)" Endif Endif ; get the file's size Size = FileSize(sDriveFileName) ; force it to floating point (as file sizes larger than 2GB will be in floating point) Size = Size + 0.0 ; diagnostic, if needed ;DiagnosticLine = StrCat("udfProcessFile OwnerObjectName = ", OwnerObjectName, " Size = ", Size) ;Message("udfProcessFile", DiagnosticLine) ; ; add the file's size to its creator's/owner's total size and count it udfAddToUserArray(OwnerObjectName, Size, UserArray) ; diagnostic, if needed ;DiagnosticLine = StrCat("after returning from udfAddToUserList, UserList = ", UserList) ;Message("udfProcessFile", DiagnosticLine) EndIf Return #EndFunction ; ;=================================================================================================== ; #DefineFunction udfAddToUserArray(OwnerObjectName, Size, UserArray) ; an entry (row) may or may not exist for owner ; so ; try to locate the owner entry in the used portion of the user array ; (note: current UserCount is stored/maintained in first (0th) row of array) ; if not, add newly encountered owner to the array ; ; as files within a given directory are often going to be owned by the same owner, ; optimize this by checking to see if current file's owner is same as that of previous file ; and if it is, adjust the starting value of the following for loop to start ; at that owner's entry in the array, so won't have to loop through entire array to that point ; (note: last/previously encountered owner is maintained in the maxusers+1 row of the array ; ; current owner same as last/previously encountered owner? If (OwnerObjectName == UserArray[(UserArray[0,1] + 1), 0]) ; yes, current owner same as last/previously encountered ; so set index to last/previously's index (so don't have to loop through array to find user) UserIndex = UserArray[(UserArray[0,1] + 1), 1] Else ; no, current owner is different than last/previously encountered ; so loop through array to find current owner's position (index) in the array For UserIndex = 1 to UserArray[0,2] by 1 ; found current owner in the array? If (OwnerObjectName == UserArray[UserIndex,0]) ; matches user name already in array, so exit the loop ; this will leave userindex pointing at owner's location in array Break Endif Next Endif ; was owner already present in array (as determined by last/previous check or by looping search)? If (UserIndex > UserArray[0,2]) ; No, didn't find user/owner entry in the currently used portion of array ; Assure haven't maxed out/consumed the user portion of the array (i.e. that have room to add another user/owner) If (UserIndex == UserArray[0,1]) ; have exceeded MaximumUsers amount, report it and abend ; (for batch/command line execution, need to output to file instead of console?) MessageLine = StrCat("Exceeded UserArray maximum size of ", UserArray[0,1]) Message(UserArray[0,0], MessageLine) Exit Else ; still have space in the array to add another user/owner (have not exceeded maximumusers) ; index is pointing at first unused entry following last added user entry in the array ; so no matching owner/user was found in array, ; so go ahead and add/start a new entry for this user, in the unused entry UserArray[UserIndex,0] = OwnerObjectName ; owner/user name UserArray[UserIndex,1] = Size + 0.0 ; space used (0.0 forces floating point) UserArray[UserIndex,2] = 1 ; file count ; and set UserCount value in header row to current UserIndex ; (or could just increment it by one) UserArray[0,2] = UserIndex Endif Else ; Yes, did find user/owner entry in the currently used portion of array ; so increase total file size and file count per current file for the user UserArray[UserIndex,1] = UserArray[UserIndex,1] + Size + 0.0 ; (0.0 forces floating point) UserArray[UserIndex,2] = UserArray[UserIndex,2] + 1 Endif ; set last/previous owner name and index (maintained in row maximumusers+1 of array) ; to that of current owner, for use in next optimized "same as previous owner?" test UserArray[(UserArray[0,1] + 1), 0] = OwnerObjectName UserArray[(UserArray[0,1] + 1), 1] = UserIndex ; ; diagnostic, if needed ;DiagnosticLine = StrCat("UserIndex = ", UserIndex) ;DiagnosticLine = StrCat(DiagnosticLine, " user name = X", UserArray[UserIndex,0], "X ") ;DiagnosticLine = StrCat(DiagnosticLine, " space used = ", UserArray[UserIndex,1]) ;DiagnosticLine = StrCat(DiagnosticLine, " file count = ", UserArray[UserIndex,2]) ;Message(UserArray[0,0], DiagnosticLine) Return #EndFunction ; ; ;=================================================================================================== ; #DefineFunction SaveFileAsSpreadsheet(OutputFileNameArray, SpreadsheetFileName, DialogTitle) ; note: might be better to do the following via OLE extender ; for reliability, timing/speed, additional formatting, column totals, etc. sake? ; ; start the spreadhsheet program and wait for it to be ready Run("excel.exe", "") TimeDelay(5) WinWaitExist("~Microsoft Excel", 60) ; start a new sheet (to be safe, in case Excel is already being used) ; File | New | OK and wait for it to be ready SendKeysTo("~Microsoft Excel", "!fn{ENTER}") TimeDelay(5) ; import the delimited text file (doing this, instead of opening the file, ; causes the column widths to be adjusted according to incoming data values maximum width) ; Data | Get External Data | Import Text File ; (copy/paste the output file name into the entry field, as paste is faster than typein) ClipPut(OutputFileNameArray[0]) SendKeysTo("~Microsoft Excel", "!ddt^v{ENTER}") TimeDelay(10) ; supply the import the next, next, next, ... that it needs SendKeysTo("~Microsoft Excel", "{ENTER}{ENTER}{ENTER}{ENTER}{ENTER}") TimeDelay(10) ; save the sheet out as spreadsheet format file ; (copy/paste the spreadsheet file name into the entry field, as paste is faster than typein) ClipPut(SpreadsheetFileName) SendKeysTo("~Microsoft Excel", "!fa^v{ENTER}") TimeDelay(10) ; let the user know the sheet has been saved and can now be viewed, etc. as they wish MessageLine = "Spreadsheet has been saved. View or close as you wish." Message(DialogTitle, MessageLine) Return #EndFunction ; ;=================================================================================================== Return ; return from interpreter's gosub fallthru of UDFs ; ; so that UDFs can be placed below main code ;===================================================================================================
Article ID: W16056
File Created: 2004:03:30:15:42:38
Last Updated: 2004:03:30:15:42:38