-- CatalogImpl.Mesa
-- Gifford, July 16, 1982 10:42 am
-- Schroeder, September 20, 1982 1:00 pm

DIRECTORY
DirMan USING [Dir, Open, Close, Insert, SetFlushMode],
CatalogComm,
CIFS USING [AddSearchRule, DeleteContext,
Enumerate, EProc, Error, GetSearchRules, GetWDir, SetWDir],
ConvertUnsafe USING [ToRope],
Environment USING [Comparison],
FileIO USING [Open, OpenFailed],
IO USING [char, Close, Handle, int, PutChar, PutF, PutFR, rope, string],
List USING [CompareProc, Sort],
LongString USING [EquivalentStrings],
Process USING [Yield],
Rope USING [Cat, Compare, Equal, Fetch, Find, FromRefText, Length,
Lower, Replace, ROPE, Substr, Upper],
Storage USING [StringLength],
STP USING [Error, FileInfo, FileInfoObject],
UECP,
UserExec USING [CommandProc, GetNameAndPassword, RegisterCommand, GetStreams];

CatalogImpl: PROGRAM
IMPORTS DirMan, CatalogComm, CIFS, ConvertUnsafe,
 FileIO, IO, List, LongString, Process,
 Rope, Storage, STP, UECP, UserExec
EXPORTS CatalogComm = {

-- MDS Usage.
ch: CatalogComm.Handle;
-- MDS Usage.

CatalogError: PUBLIC ERROR[e: CatalogComm.CatalogErrorType] = CODE;
NoneFound: PUBLIC SIGNAL = CODE;

-- Why things are STRINGs instead of Rope.ROPEs:
-- STP calls use STRING in their interfaces.

Main: PROC [h: CatalogComm.Handle] = {

-- Cleanup is called both before and after doing work.
Cleanup: PROC = {
h.fCount ← 0;
h.locEnumFileName ← NIL;
h.directory ← NIL;
h.filesystem ← NIL;
h.current ← NIL;
CatalogComm.FreeWorld[h];
};

{
ENABLE {
 UNWIND => Cleanup[];
 ABORTED => {
  h.out.PutF["Catalog: Aborted!\n"];
  GOTO leave;
  };
 STP.Error => {
  h.out.PutF["Catalog STP Error: %s, Code: %s\n", IO.string[error], IO.char[reply]];
  IF h.reportSignals THEN REJECT ELSE GOTO leave;
  };
 CIFS.Error => CHECKED {
  h.out.PutF["Catalog CIFS Error: %s, Code: %s\n", IO.rope[error], IO.char[reply]];
  IF h.reportSignals THEN REJECT ELSE GOTO leave;
  };
 CatalogComm.CatalogError => {
  IF e=catalogAbort THEN {
   h.out.PutF["Catalog: Aborted!\n"];
   GOTO leave;
   };
  h.out.PutF["Catalog CatalogError: %s\n", IO.rope[CatalogComm.CatalogErrorString[e]]];
  IF h.reportSignals THEN REJECT ELSE GOTO leave;
  };
 ANY => {
IF h.reportSignals THEN REJECT ELSE h.out.PutF["Catalog: Unknown error\n"];
GOTO leave;
};
 };
h.cmdindex ← 0;
h.oldWorkingDir ← CIFS.GetWDir[];
h.oldSearchRules ← CIFS.GetSearchRules[];
h.argv ← UECP.Parse[h.he.commandLine];
CIFS.DeleteContext[];
RestoreDefaults[h];
-- Main loop, parse command and do work
DO
h.cmdindex ← h.cmdindex+1;
Cleanup[];
IF h.cmdindex>=h.argv.argc THEN GOTO leave;
IF h.argv[h.cmdindex].Length[]=0 THEN GOTO leave;
IF Rope.Equal[h.argv[h.cmdindex], "Catalog", FALSE] THEN LOOP;
IF h.argv[h.cmdindex].Fetch[0]='- THEN {
FOR i: INT ← 1, i+1 WHILE i<h.argv[h.cmdindex].Length[] DO
SELECT Rope.Lower[h.argv[h.cmdindex].Fetch[i]] FROM
'i => { h.saveLinksComments ← FALSE; };
'n => { h.doStoreRemote ← FALSE; };
'l => { h.listLocal ← TRUE; };
's => { h.reportSignals ← TRUE; };
'r => { h.readLocal ← TRUE; };
'z => { h.listDirsOnly ← TRUE; };
'v => { h.verbose ← TRUE; };
ENDCASE => GOTO usage;
ENDLOOP;
LOOP; -- Get next command line token
};
IF h.argv[h.cmdindex].Fetch[0]#'/ THEN GOTO usage;
h.filesystem ← CatalogComm.SubStringCopy[s: h.argv[h.cmdindex],
 start: 1,
 stop: CatalogComm.FindFirst[s: h.argv[h.cmdindex], c: '/, start: 2]-1
  ! NoneFound => GOTO usage];
h.directory ← CatalogComm.SubStringCopy[s: h.argv[h.cmdindex],
 start: CatalogComm.FindFirst[s: h.argv[h.cmdindex], c: '/, start: 2]+1,
 stop: IF h.argv[h.cmdindex].Fetch[h.argv[h.cmdindex].Length[]-1] = '/ THEN h.argv[h.cmdindex].Length[]-2 ELSE h.argv[h.cmdindex].Length[]-1];

h.out.PutF["Cataloging /%s/%s/\n", IO.rope[h.filesystem], IO.rope[h.directory]];
h.locEnumFileName ← IO.PutFR["%s-%s.enum", IO.rope[h.filesystem], IO.rope[h.directory]];
FOR i: INT ← 0, i+1 WHILE i<h.locEnumFileName.Length[] DO
IF h.locEnumFileName.Fetch[i]='/ THEN
h.locEnumFileName ← Rope.Replace[base: h.locEnumFileName,
     start: i, len: 1, with: "-"];
ENDLOOP;
-- If h.readLocal then we want to read from an already existing local enumeration file.
--Otherwise we want to enumerate the remote directory and create a local one.

h.out.PutF["One '. for every 10 files enumerated\n"];

IF h.readLocal THEN CatalogComm.EnumFromLocalFile[h]
ELSE CatalogComm.EnumFromRemoteFile[h];

h.out.PutChar['\n]; -- In case ParseFile printed any dots

-- At this point, all the files have been entered in the appropriate directory file
--but the directory entries for the directories still need to be added.

IF CatalogComm.CheckForAbort[h] THEN ERROR CatalogComm.CatalogError[catalogAbort];
h.out.PutF["Catalog: Making directory entries for directories.\n"];
EnterDirectories[h];
IF CatalogComm.CheckForAbort[h] THEN ERROR CatalogComm.CatalogError[catalogAbort];
IF h.saveLinksComments THEN {
h.out.PutF["Catalog: Scanning old directories for existing links and comments\n"];
EnterLinksAndOrComments[h];
};
IF CatalogComm.CheckForAbort[h] THEN ERROR CatalogComm.CatalogError[catalogAbort];

-- now the whole structure is built in memory.
-- Sort each directory
h.out.PutF["Catalog: Sorting directories.\n"];
SortDirectories[h];
IF CatalogComm.CheckForAbort[h] THEN ERROR CatalogComm.CatalogError[catalogAbort];
IF h.doStoreRemote THEN { h.out.PutF["Catalog: Writing remote dir.bt files.\n"]; StoreRemote[h]; };
IF CatalogComm.CheckForAbort[h] THEN ERROR CatalogComm.CatalogError[catalogAbort];

IF h.listLocal OR h.listDirsOnly THEN h.out.PutF["*nCatalog Directory:\n----------\n/%s/%s/\n", IO.rope[h.filesystem], IO.rope[h.directory]];
IF h.listDirsOnly THEN h.out.PutF["Columns are files, subdirectories, links, and comments.\n"];
IF h.listLocal OR h.listDirsOnly THEN CatalogComm.CatalogList[h, h.directory, h.directory, 3, h.listDirsOnly];
RestoreDefaults[h];
ENDLOOP; --end of big command evaluation loop

EXITS
leave => NULL;
usage => h.out.PutF["usage: Catalog [-switches] /hostname/directory/names/ (multilevel ok)\n"];
};
Cleanup[];
CIFS.SetWDir[h.oldWorkingDir];
h.oldWorkingDir ← NIL;
WHILE h.oldSearchRules#NIL DO
CIFS.AddSearchRule[path: h.oldSearchRules.first, before: FALSE];
h.oldSearchRules ← h.oldSearchRules.rest;
ENDLOOP;
h.out.PutF["Catalog -- Goodbye\n"];
};

EnumFromLocalFile: PUBLIC PROC [h: CatalogComm.Handle] = {
aFileName: STRING ← [100];
aFile: STP.FileInfoObject;
localDirectory: STRING ← [100];
localVersion: STRING ← [20];
localName: STRING ← [100];
sdir, edir, bang: NAT;
locEnumStream: IO.Handle;
aFile.directory ← localDirectory;
aFile.body ← localName;
aFile.version ← localVersion;
locEnumStream ← FileIO.Open[h.locEnumFileName, read ! FileIO.OpenFailed => CHECKED {
 h.out.PutF["Catalog: Can't find local file!\n"];
ERROR CatalogComm.CatalogError[cantEnumerate];
}];
h.out.PutF["Catalog: Building directories from local enumeration file %s\n", IO.rope[h.locEnumFileName]];

WHILE CatalogComm.GetLine[h, locEnumStream, aFileName] DO {
Process.Yield[];
IF CatalogComm.CheckForAbort[h] THEN {
locEnumStream.Close[];
ERROR CatalogComm.CatalogError[catalogAbort];
};
aFile.directory.length ← aFile.body.length ← 0;
{
ever: NAT;
bang ← CatalogComm.LFindLast[aFileName, '! ! NoneFound => GOTO NoVersion];
ever ← bang + 1;
WHILE aFileName[ever] IN ['0..'9] AND ever < aFileName.length DO
ever ← SUCC[ever];
ENDLOOP;
localVersion.length ← 0;
aFile.version ← localVersion;
CatalogComm.LSubStringAppend[from: aFileName, to: aFile.version, start: bang+1, stop: ever-1];
EXITS
NoVersion => {bang ← aFileName.length; aFile.version ← NIL; };
};
edir ← CatalogComm.LFindLast[aFileName, '>];
CatalogComm.LSubStringAppend[from: aFileName, to: aFile.body, start: edir+1, stop: bang-1];
sdir ← CatalogComm.LFindFirst[aFileName, '<, 0];
CatalogComm.LSubStringAppend[from: aFileName, to: aFile.directory, start: sdir+1, stop: edir-1];
ParseFile[h: h, vf: @aFile];
};
ENDLOOP;
locEnumStream.Close[];
};

MakeCurrent: PROC [h: CatalogComm.Handle, key: Rope.ROPE, createIfNotFound: BOOL] = {
p: LIST OF REF CatalogComm.Directory;
-- cache most recently found directory as h.current.
IF h.current#NIL AND Rope.Compare[s1: key, s2: h.current.name, case: FALSE]=equal
THEN RETURN;
FOR p ← h.root, p.rest WHILE p#NIL DO
Process.Yield[];
IF Rope.Compare[s1: p.first.name, s2: key, case: FALSE]=equal THEN EXIT;
ENDLOOP;
IF p = NIL THEN {
IF NOT createIfNotFound THEN { h.current ← NIL; RETURN; };
h.current ← NEW[CatalogComm.Directory];
h.root ← CONS[h.current, h.root];
h.current.name ← key;
h.current.chain ← NIL;
}
ELSE h.current ← p.first;
};

ParseFile: PUBLIC PROC [h: CatalogComm.Handle, vf: STP.FileInfo] = {
t: REF CatalogComm.Node;

Process.Yield[];
IF h.fCount=0 THEN h.out.PutChar['.];
h.fCount ← h.fCount + 1;
IF h.fCount>=10 THEN h.fCount ← 0;

IF Storage.StringLength[vf.directory]=0 THEN ERROR CatalogComm.CatalogError[nilDirectory];
IF Storage.StringLength[vf.body]=0 THEN ERROR CatalogComm.CatalogError[nilFilename];
-- When making new directories, note old ones but don't make a node.
FOR i: NAT IN [0..vf.directory.length) DO
IF vf.directory[i] = '> THEN vf.directory[i] ← '/;
ENDLOOP;
IF h.current=NIL OR NOT AlreadyCurrent[s: vf.directory, r: h.current.name] THEN
MakeCurrent[h: h, key: ConvertUnsafe.ToRope[vf.directory], createIfNotFound: TRUE];
IF LongString.EquivalentStrings[vf.body, "dir.bt"] THEN {
h.current.oldDirExists ← TRUE;
}
ELSE {
t ← NEW[CatalogComm.Node];
t.what ← file;
t.directory ← NIL;
FOR i: NAT IN [0..vf.body.length) DO
IF vf.body[i] = '> THEN vf.body[i] ← '/;
 ENDLOOP;
IF Storage.StringLength[vf.version]#0 THEN t.name ← IO.PutFR["%s!%s", IO.string[vf.body], IO.string[vf.version]]
ELSE t.name ← ConvertUnsafe.ToRope[vf.body];
h.current.fileCount ← SUCC[h.current.fileCount];
h.current.chain ← CONS[t, h.current.chain];
};
};

AlreadyCurrent: PROC [s: LONG STRING, r: Rope.ROPE] RETURNS [BOOL] = {
slen: NAT ← IF s = NIL THEN 0 ELSE s.length;
IF slen # r.Length[] THEN RETURN [FALSE];
FOR i: NAT IN [0..slen) DO
IF s[i]#r.Fetch[i] THEN RETURN[FALSE];
ENDLOOP;
RETURN[TRUE];
};

EnterDirectories: PROC [h: CatalogComm.Handle] = {
topLevelFound: BOOL ← FALSE;
parent: Rope.ROPE ← NIL;
entry: Rope.ROPE ← NIL;
lastGR: INT;
madeDirectory: BOOL;
t: REF CatalogComm.Node;
p: LIST OF REF CatalogComm.Directory;
-- For each directory on the list, if there is a parent listed, make an entry in it.
DO
madeDirectory ← FALSE;
FOR p ← h.root, p.rest WHILE p # NIL DO
Process.Yield[];
IF p.first.entryMade THEN LOOP; -- We've already handled this one.
-- we can't make an entry for the top level directory in it's parent directory, so skip it.
-- This stuff happens if the enumerated directory has a single component name.
lastGR ← CatalogComm.FindLast[s: p.first.name, c: '/ ! NoneFound => {
IF Rope.Compare[s1: p.first.name, s2: h.directory, case: FALSE]=equal THEN { -- found the one we expect
IF topLevelFound THEN ERROR CatalogComm.CatalogError[topLevelDirectoryFoundTwice];
topLevelFound ← TRUE;
p.first.entryMade ← TRUE;
LOOP;
}
ELSE ERROR CatalogComm.CatalogError[unknownTopLevelDirectory];
} ];
parent ← CatalogComm.SubStringCopy[s: p.first.name, start: 0, stop: lastGR-1];
entry ← CatalogComm.SubStringCopy[s: p.first.name, start: lastGR+1, stop: p.first.name.Length[]-1];
MakeCurrent[h, parent, FALSE];
-- This case happens when the enumerated directory has a multiple component name.
-- Or when a directory contains only other directories.
IF h.current = NIL THEN {
-- Top level dir, probably, should be only one
IF Rope.Compare[s1: p.first.name, s2: h.directory, case: FALSE]=equal THEN {
IF topLevelFound THEN ERROR CatalogComm.CatalogError[topLevelDirectoryFoundTwice];
topLevelFound ← TRUE;
p.first.entryMade ← TRUE;
LOOP;
}
ELSE {
madeDirectory ← TRUE;
MakeCurrent[h, parent, TRUE]; -- create an intervening directory
};
};
t ← NEW[CatalogComm.Node];
t.what ← directory;
t.directory ← p.first.name; -- full path name, needed for lister
t.name ← Rope.Cat[entry, "/"];
h.current.chain ← CONS[t, h.current.chain];
h.current.directoryCount ← SUCC[h.current.directoryCount];
p.first.entryMade ← TRUE;
ENDLOOP;
IF NOT madeDirectory THEN EXIT;
ENDLOOP;
};


SortDirectories: PROC [h: CatalogComm.Handle] = {
fc, dc, ad, lc, cc: INT ← 0;
p: REF CatalogComm.Directory;
pp: LIST OF REF CatalogComm.Directory;

-- Count up the files and directories
FOR pp ← h.root, pp.rest WHILE pp # NIL DO
Process.Yield[];
h.out.PutChar['.];
p ← pp.first;
fc ← fc + p.fileCount;
dc ← dc + p.directoryCount;
lc ← lc + p.linkCount;
cc ← cc + p.commentCount;
ad ← SUCC[ad];
p.chain ← List.Sort[p.chain, LessThan];
ENDLOOP;
h.out.PutF["\nCatalog: %d files, %d directories, %d links, and %d comments in %d directories.\n", IO.int[fc], IO.int[dc], IO.int[lc], IO.int[cc], IO.int[ad]];
};


-- Sort by directory over name, then by name, then decreasing by version.
LessThan: List.CompareProc = CHECKED {
p: REF CatalogComm.Node ← NARROW[ref1];
q: REF CatalogComm.Node ← NARROW[ref2];
i: Environment.Comparison ← Rope.Compare[s1: p.name, s2: q.name, case: FALSE];
IF i=equal THEN SELECT TRUE FROM
p.what=directory AND q.what=file=> i ← less;
p.what=file AND q.what=directory => i ← greater;
ENDCASE => NULL;
RETURN[i]
};

StoreRemote : PROC [h: CatalogComm.Handle] = {
t: REF CatalogComm.Node;
tt: LIST OF REF;
p: REF CatalogComm.Directory;
pp: LIST OF REF CatalogComm.Directory;
dFH: DirMan.Dir ← NIL;
{ENABLE UNWIND => {
IF dFH#NIL THEN dFH.Close[];
};
FOR pp ← h.root, pp.rest WHILE pp # NIL DO
Process.Yield[];
p ← pp.first;
IF h.verbose THEN h.out.PutF[" %s", IO.rope[p.name]]
ELSE h.out.PutChar['.];
dFH ← DirMan.Open[name: Rope.Cat["/", ch.filesystem, "/", p.name], erase: TRUE];
dFH.SetFlushMode[flush: FALSE];
FOR tt ← p.chain, tt.rest WHILE tt#NIL DO
t ← NARROW[tt.first];
dFH.Insert[name: t.name, link: t.target, comment: t.comment];
ENDLOOP;
dFH.Close[];
ENDLOOP;
h.out.PutChar['\n];
};
};

EnterLinksAndOrComments: PROC [h: CatalogComm.Handle] = {
tt: LIST OF REF CatalogComm.Directory;
t: REF CatalogComm.Directory;
FOR tt ← h.root, tt.rest WHILE tt#NIL DO
t ← tt.first;
IF NOT t.oldDirExists THEN LOOP;
-- There exists a previous dir.bt file
IF h.verbose THEN h.out.PutF["%s ", IO.rope[t.name]]
ELSE h.out.PutChar['.];
EnterLCInternal[h: h, dir: t];
ENDLOOP;
h.out.PutChar['\n];
};

EqualTextRope: SAFE PROC [t: REF READONLY TEXT, r: Rope.ROPE, case: BOOL]
RETURNS [BOOL] = CHECKED {
tlen: NAT ← IF t = NIL THEN 0 ELSE t.length;
IF tlen # r.Length[] THEN RETURN [FALSE];
IF case THEN FOR i: NAT IN [0..tlen) DO
IF t[i]#r.Fetch[i] THEN RETURN[FALSE];
ENDLOOP
ELSE FOR i: NAT IN [0..tlen) DO
IF Rope.Upper[t[i]]#Rope.Upper[r.Fetch[i]] THEN RETURN[FALSE];
ENDLOOP;
RETURN[TRUE];
};

EnterLCInternal: PROC [h: CatalogComm.Handle, dir: REF CatalogComm.Directory] = CHECKED {
dirName: Rope.ROPE;
MyEP: CIFS.EProc = {
pp: LIST OF REF;
p: REF CatalogComm.Node;
directory: BOOL ← name[name.length-1]='/;
ll: NAT ← IF link=NIL THEN 0 ELSE link.length;
cl: NAT ← IF comment=NIL THEN 0 ELSE comment.length;
IF ll=0 AND cl=0 THEN RETURN[stop: FALSE];
FOR pp ← dir.chain, pp.rest WHILE pp#NIL DO
p ← NARROW[pp.first];
IF directory # (p.what=directory) THEN LOOP;
-- names must match exactly or up to version number of actual file
-- If old dir.dir entry has a version, then must match exactly.
-- If old dir.dir did not have a version, then must match any version.
IF EqualTextRope[t: name, r: p.name, case: FALSE] OR
EqualTextRope[
  t: name,
  r: Rope.Substr[base: p.name, start: 0, len: MAX[Rope.Find[s1: p.name, s2: "!"], 0]],
  case: FALSE] THEN {
-- Discard an old link for something that is now real
IF cl#0 THEN {
  p.comment ← Rope.FromRefText[comment];
  dir.commentCount ← dir.commentCount + 1;
  };
};
REPEAT
FINISHED => {
-- If it's not a link, then discard it because it's a comment for a deleted item
IF ll#0 THEN {
  p ← NEW[CatalogComm.Node];
p.what ← link;
  p.directory ← dir.name;
  p.name ← Rope.FromRefText[name];
  p.target ← Rope.FromRefText[link];
  p.comment ← Rope.FromRefText[comment];
  dir.chain ← CONS[p, dir.chain];
  dir.linkCount ← dir.linkCount + 1;
  IF cl#0 THEN dir.commentCount ← dir.commentCount + 1;
  };
};
ENDLOOP;
RETURN[stop: FALSE];
};
dirName ← IO.PutFR["/%s/%s/", IO.rope[h.filesystem], IO.rope[dir.name]];
CIFS.Enumerate[dir: dirName, pattern: "*", p: MyEP ! CIFS.Error => CONTINUE];
};

RestoreDefaults: PROC [h: CatalogComm.Handle] = {
h.reportSignals ← FALSE;
h.doStoreRemote ← TRUE;
h.listLocal ← FALSE;
h.readLocal ← FALSE;
h.listDirsOnly ← FALSE;
h.saveLinksComments ← TRUE;
h.verbose ← FALSE;
};

LoopMain: UserExec.CommandProc = TRUSTED {
ch ← NEW[CatalogComm.CatalogObject];
[ch.in, ch.out] ← exec.GetStreams[];
ch.eh ← exec;
ch.he ← event;
[name: ch.name, password: ch.password] ← UserExec.GetNameAndPassword[];
Main[ch];
};

Init: PROC = {
UserExec.RegisterCommand["Catalog.~", LoopMain, "Construct CIFS directories", docstring];
};

docstring: Rope.ROPE = "Command format:
Catalog accepts commands of the form
Catalog {[-switches] remotePathName}<CR> where a remote path name has the form
/HostName/Directory/. Directory can include '/ for subdirectories.
The braces indicate repeated commands are OK. Switches must be repeated (or changed) for repeated commands.
The switches are (default values in parentheses):
i Ignore exisiting comments and links (FALSE)
n Don't store new directories. (FALSE)
l Produce nested listing of enumeration. (FALSE)
r Read local enumeration file. (FALSE)
s Report SIGNALs to debugger (FALSE)
z Produce nested listing of enumeration, directories only. (FALSE)
v List names of directories as they are handled. (FALSE)

Catalog will create a local enumeration file. Reading it (with -r) is faster than a second remote enumeration if the need arises to do it twice.\n";

-- main program

Init[];
}.
25-Jul-81 22:50:58, Stewart, modified from MTypeImpl.mesa
26-Jul-81 0:54:22, Stewart, stuff added from dfdiffimpl.mesa
30-Jul-81 10:18:37, Stewart, added arrays of files
30-Jul-81 10:51:18, Stewart, EquivalentString used!
8-Aug-81 17:38:25, Stewart, Read from Disk instead of FTP Enumerate
8-Aug-81 18:42:22, Stewart, create directory files without dir links
9-Aug-81 21:35:39, Stewart, make dir entries for dirs
11-Aug-81 21:31:01, Stewart, modify LessThan
12-Aug-81 23:18:55, Stewart, BubbleSort, recursive print procedure
15-Aug-81 16:39:46, Stewart, command line switches, remote files
  ignore dir.dir files
15-Aug-81 19:12:21, Stewart, combined Enumeration
16-Aug-81 19:20:36, Stewart, General cleanup
24-Aug-81 18:57:08, Stewart, Cedarize
27-Aug-81 1:04:21, Stewart, More Cedarize
29-Aug-81 23:31:47, Stewart, PF
30-Aug-81 0:39:18, Stewart, Split into two parts with interface
2-Sep-81 0:14:36, Stewart, creates intervening directories if needed.
2-Sep-81 1:43:08, Stewart, directory listings only
2-Sep-81 1:54:52, Stewart, added limit to length of local file name
  since Pilot Directory breaks
3-Sep-81 0:29:51, Stewart, FREE all the objects
3-Sep-81 1:14:07, Stewart, Use CatalogObject
4-Sep-81 21:09:34, Stewart, numbered local files plus command file
5-Sep-81 16:08:41, Stewart, Yields added
17-Oct-81 22:38:38, Stewart, New Cedar, STP replaces FTP
1-Nov-81 17:37:04, Stewart, Add space after names in dir.dir files, for
    future comments and links
9-Feb-82 23:14:41, Stewart, Cedar 2.3, IO, comments and links
March 21, 1982 3:08 pm, Stewart, Cedar 2.5.1, Viewers, CIFS
25-Mar-82 15:58:48, Stewart, remove Subr, STPSubr
March 26, 1982 5:38 pm, Stewart, bulletproofing
June 16, 1982 9:58 am, Gifford, new BTree directory system

Questions:

It shouldn't be necessary to store a copy of the directory name in EACH Node.directory of type file,
because that is stored in the Directory.name. However, the Node.directory IS needed for Nodes
of type directory. It is uded by the lister.

Problems:
STP doesn't connect to a directory when needed