FileTransferCommandsImpl: 
CEDAR MONITOR
IMPORTS CIFS, Commander, FileIO, FileLookup, FtpMan, IO, Process, RefText, DirectoryList, Rope, UserCredentials
~ BEGIN
defaultStoreTextServer: Rope.ROPE ← "Maxc";
defaultPublicServer: Rope.ROPE ← "Indigo";
defaultPersonalServer: Rope.ROPE ← "Ivy";
pauseAmount: Process.Milliseconds ← 1000;
DoOperationProc: TYPE ~ PROC [host, directory, filename: Rope.ROPE, log: IO.STREAM] RETURNS [quit: BOOLEAN ← FALSE];
StoreTextCommand: Commander.CommandProc ~ {DoOpForEachFile[StoreTextOp, cmd, defaultStoreTextServer, defaultStoreTextServer, local]};
StoreCommand: Commander.CommandProc ~ {DoOpForEachFile[StoreOp, cmd, defaultPublicServer, defaultPersonalServer, local]};
RetrieveCommand: Commander.CommandProc ~ {DoOpForEachFile[RetrieveOp, cmd, defaultPublicServer, defaultPersonalServer, remote]};
ListRemoteCommand: Commander.CommandProc ~ {DoOpForEachFile[ListRemoteOp, cmd, defaultPublicServer, defaultPersonalServer, remoteAll]};
ListVerboseCommand: Commander.CommandProc ~ {DoOpForEachFile[ListVerboseOp, cmd, defaultPublicServer, defaultPersonalServer, remoteAll]};
StoreTextOp: 
ENTRY DoOperationProc ~ 
TRUSTED {
ENABLE UNWIND => NULL; {ENABLE CIFS.Error => {log.PutRope[" "]; log.PutRope[error]; log.PutRope["\n"]; GOTO Quit};
tempStream: IO.STREAM ← FileIO.Open["StoreText$$$", overwrite];
source: 
IO.
STREAM ← FileIO.Open[filename ! FileIO.OpenFailed => 
CHECKED {
log.PutRope[fileName];
IF why = fileNotFound THEN log.PutRope[" not found.\n"]
ELSE IF why = illegalFileName THEN log.PutRope[": illegal file name.\n"]
ELSE log.PutRope[": open error.\n"];
GOTO Quit
}];
 
text: REF TEXT ← RefText.ObtainScratch[1024];
version: INT;
WHILE 
NOT source.EndOf 
DO
nBytesRead: NAT ← source.GetBlock[text];
tempStream.PutBlock[text, 0, nBytesRead];
ENDLOOP;
 
tempStream.Close;
source.Close;
RefText.ReleaseScratch[text];
log.PutF["Storing text into [%g]%g%g", IO.rope[host], IO.rope[directory], IO.rope[filename]];
version ← FtpMan.Store[host, PutInSlashes[directory].Concat[filename], FileIO.CapabilityFromStream[FileIO.Open["StoreText$$$"]]];
log.PutF["!%g\n", IO.int[version]];
EXITS Quit => {quit←TRUE}}};
 
StoreOp: DoOperationProc ~ 
TRUSTED {
ENABLE UNWIND => NULL; {ENABLE CIFS.Error => {log.PutRope[" "]; log.PutRope[error]; log.PutRope["\n"]; GOTO Quit};
source: CIFS.OpenFile ← CIFS.Open[filename, CIFS.read];
version: INT;
log.PutF["Storing [%g]%g%g", IO.rope[host], IO.rope[directory], IO.rope[filename]];
version ← FtpMan.Store[host, PutInSlashes[directory].Concat[filename], CIFS.GetFC[source]];
source.Close;
log.PutF["!%g\n", IO.int[version]];
EXITS Quit => {quit←TRUE}}};
 
RetrieveOp: DoOperationProc ~ 
TRUSTED {
ENABLE UNWIND => NULL; {ENABLE CIFS.Error => {log.PutRope[" "]; log.PutRope[error]; log.PutRope["\n"]; GOTO Quit};
dest: CIFS.OpenFile ← CIFS.Open[RemoveVersionNumber[filename], CIFS.replace];
log.PutF["Retrieving [%g]%g%g . . . ", IO.rope[host], IO.rope[directory], IO.rope[filename]];
FtpMan.Retrieve[host, PutInSlashes[directory].Concat[filename], CIFS.GetFC[dest]];
dest.Close;
log.PutF["Done.\n"];
EXITS Quit => {quit←TRUE}}};
 
ListRemoteOp: DoOperationProc ~ {
log.PutF["[%g]%g%g\n", IO.rope[host], IO.rope[directory], IO.rope[filename]];
};
 
paddedNameLength: INT ← 41;
ListVerboseOp: DoOperationProc ~ {
result: FileLookup.Result;
version: CARDINAL;
create: System.GreenwichMeanTime;
count: LONG CARDINAL;
nameLength: INT ~ host.Length + directory.Length + filename.Length + 2;
log.PutF["[%g]%g%g", IO.rope[host], IO.rope[directory], IO.rope[filename]];
THROUGH [nameLength..paddedNameLength) DO log.PutChar[' ] ENDLOOP;
[result: result, version: version, create: create, count: count] ← FileLookup.LookupFile[host, directory.Concat[filename]];
SELECT result 
FROM
noResponse => log.PutF["no response from %g.\n", IO.rope[host]];
noSuchName => log.PutRope["does not exist.\n"];
noSuchPort => log.PutRope["no such port.\n"];
ok => log.PutF[" Of %g --%5d\n", IO.time[create], IO.card[count]];
ENDCASE => ERROR;
 
};
 
ParsePath: 
PROC [name: Rope.
ROPE] 
RETURNS [host, directory, filename: Rope.
ROPE ← 
NIL] ~ {
hostStart, hostEnd, directoryStart, directoryEnd: INT ← 0;
filenameStart, filenameEnd: INT ← name.Length;
The interprtation of state is as follows:
0 [ 1 ] 2 < 3 > 4
 
state: NAT ← 0;
loc: INT ← 0;
Action: 
PROC [c: 
CHAR] 
RETURNS [
BOOL←
FALSE] ~ {
SELECT state 
FROM
0 => 
IF c='[ 
THEN {hostStart ← loc+1; state ← 1}
ELSE IF c='< THEN {directoryStart ← loc; directoryEnd ← name.Length; state ← 3}
ELSE {directoryStart ← directoryEnd ← filenameStart ← loc; state ← 3};
 
1 => IF c='] THEN {hostEnd ← loc; state ← 2} ELSE IF NOT Rope.Letter[c] THEN RETURN [TRUE];
2 => 
IF c='< 
THEN {directoryStart ← loc; directoryEnd ← name.Length; state ← 3}
ELSE {directoryStart ← directoryEnd ← filenameStart ← loc; state ← 3};
 
3 => IF c='> THEN {directoryEnd ← filenameStart ← loc+1};
ENDCASE => ERROR;
 
loc ← loc+1;
};
 
IF name.Map[action: Action] OR state # 3 THEN RETURN;
host ← name.Substr[start: hostStart, len: hostEnd-hostStart];
directory ← name.Substr[start: directoryStart, len: directoryEnd-directoryStart];
filename ← name.Substr[start: filenameStart, len: filenameEnd-filenameStart];
};
 
PutInSlashes: 
PROC [dir: Rope.
ROPE] 
RETURNS [Rope.
ROPE] ~ {
Translator: 
PROC [old: 
CHAR] 
RETURNS [new: 
CHAR] ~ {
new ← IF old = '> THEN '/ ELSE old;
};
 
RETURN [dir.Translate[start: 1, translator: Translator]];
};
 
TakeOutSlashes: 
PROC [dir: Rope.
ROPE] 
RETURNS [Rope.
ROPE] ~ {
Translator: 
PROC [old: 
CHAR] 
RETURNS [new: 
CHAR] ~ {
new ← IF old = '/ THEN '> ELSE old;
};
 
RETURN [Rope.Concat["<", dir.Translate[translator: Translator]]];
};
 
GetUserName: 
PROC 
RETURNS [Rope.
ROPE] ~ {
name: Rope.ROPE ← UserCredentials.GetUserCredentials[].name;
length: INT ← 0;
WHILE length<name.Length AND name.Fetch[length] # '. DO length ← length + 1 ENDLOOP;
RETURN [name.Substr[len: length]];
};
 
SupplyTopLevel: 
PROC [directory: Rope.
ROPE] 
RETURNS [Rope.
ROPE] ~ {
RETURN [IF directory.Length = 0 OR directory.Fetch[0] # '< THEN Rope.Cat["<", GetUserName[], ">", directory] ELSE directory]
};
 
RemoveVersionNumber: 
PROC [filename: Rope.
ROPE] 
RETURNS [Rope.
ROPE] ~ {
FOR i: 
INT 
IN [0..filename.Length) 
DO
IF filename.Fetch[i] = '! THEN RETURN [filename.Substr[len: i]];
ENDLOOP;
 
RETURN [filename];
};
 
CompareFileNames: 
PROC [a, b: Rope.
ROPE] 
RETURNS [Environment.Comparison] = {
versionStart: INT ← -1;
FOR i: 
INT 
IN [0..
MIN[a.Length, b.Length]) 
DO
aChar: CHAR ~ a.Fetch[i];
bChar: CHAR ~ b.Fetch[i];
IF Rope.Upper[aChar] < Rope.Upper[bChar] THEN RETURN[less];
IF Rope.Upper[aChar] > Rope.Upper[bChar] THEN RETURN[greater];
IF aChar = '! THEN {versionStart ← i+1; EXIT};
ENDLOOP;
 
IF versionStart < 0 THEN RETURN[SELECT a.Length FROM < b.Length => less, = b.Length => equal, > b.Length => greater, ENDCASE => ERROR]
ELSE {
aVersion: INT ← ValueOf[a, versionStart];
bVersion: INT ← ValueOf[b, versionStart];
RETURN[SELECT aVersion FROM < bVersion => less, = bVersion => equal, > bVersion => greater, ENDCASE => ERROR]
};
 
};
 
EqualNames: 
PROC [a, b: Rope.
ROPE] 
RETURNS [
BOOLEAN] = {
FOR i: 
INT 
IN [0..
MIN[a.Length, b.Length]) 
DO
aChar: CHAR ~ a.Fetch[i];
bChar: CHAR ~ b.Fetch[i];
IF Rope.Upper[aChar] # Rope.Upper[bChar] THEN RETURN[FALSE];
IF aChar = '! THEN RETURN[TRUE];
ENDLOOP;
 
RETURN[a.Length = b.Length];
};
 
IsNat: 
PROC [rope: Rope.
ROPE] 
RETURNS [
BOOLEAN] = {
IsNonDigit: 
PROC [c: 
CHAR] 
RETURNS [
BOOL] ~ {
RETURN[NOT (c IN ['0..'9])]
};
 
RETURN [rope.Length IN [1..4] AND NOT rope.Map[action: IsNonDigit]];
};
 
ValueOf: 
PROC [rope: Rope.
ROPE, start: 
INT ← 0] 
RETURNS [value: 
NAT ← 0] = {
Action: 
PROC [c: 
CHAR] 
RETURNS [
BOOL←
FALSE] ~ {
d: [0..10) ← c - '0;
value ← value * 10 + d;
};
 
[] ← rope.Map[action: Action, start: start];
};
 
HasVersion: 
PROC [filename: Rope.
ROPE] 
RETURNS [
BOOLEAN] ~ {
Action: 
PROC [c: 
CHAR] 
RETURNS [
BOOL] ~ {
RETURN[c = '!]
};
 
RETURN [filename.Map[action: Action]]
};
 
HasWildCard: 
PROC [filename: Rope.
ROPE] 
RETURNS [
BOOLEAN] ~ {
Action: 
PROC [c: 
CHAR] 
RETURNS [
BOOL] ~ {
RETURN[c = '*]
};
 
RETURN [filename.Map[action: Action]]
};
 
WildcardExpansion: TYPE ~ {local, remote, remoteAll};
DoOpForEachFile: 
PROC [DoOperation: DoOperationProc, cmd: Commander.Handle, defaultPublicServer: Rope.
ROPE, defaultPersonalServer: Rope.
ROPE, wildcardExpansion: WildcardExpansion] ~ {
offset: INT ← 0;
log: IO.STREAM ← cmd.out;
commands: IO.STREAM ← IO.CreateInputStreamFromRope[cmd.commandLine];
Separators: 
IO.BreakProc ~ {
RETURN[
SELECT char 
FROM
IO.SP, IO.CR, IO.TAB, ',, '; => sepr,
ENDCASE => other
]};
 
UNTIL commands.EndOf 
OR UserAbort[cmd] 
DO
name: Rope.ROPE ~ commands.GetToken[Separators];
host, directory, filename: Rope.ROPE;
IF name.Length = 0 THEN EXIT;
[host, directory, filename] ← ParsePath[name];
IF filename = NIL THEN log.PutF["Invalid name: %g\n", IO.rope[name]]
ELSE {
IF host.Length = 0 THEN host ← IF directory.Length = 0 OR directory.Fetch[0] # '< THEN defaultPersonalServer ELSE defaultPublicServer;
directory ← SupplyTopLevel[directory];
IF HasWildCard[directory] 
OR HasWildCard[filename] 
OR wildcardExpansion # local 
THEN {
IF wildcardExpansion = local 
THEN {
IF HasWildCard[directory] 
THEN 
log.PutF["%g: illegal wildcard in directory.\n", IO.rope[directory]]
 
ELSE {
AbortProc: PROC RETURNS [BOOLEAN] ~ {RETURN [UserAbort[cmd]]};
matchingFiles: LIST OF Rope.ROPE ← DirectoryList.Local[filename, AbortProc];
FOR r: 
LIST 
OF Rope.
ROPE ← matchingFiles, r.rest 
UNTIL r=
NIL 
OR UserAbort[cmd] 
DO
fname: Rope.ROPE ← r.first;
IF DoOperation[host, directory, fname, log] THEN IO.SetUserAbort[cmd.in];
ENDLOOP;
 
};
 
}
 
ELSE {
searchPattern: Rope.ROPE ← directory.Concat[filename];
AbortProc: PROC RETURNS [BOOLEAN] ~ {RETURN [UserAbort[cmd]]};
matchingFiles: LIST OF Rope.ROPE;
errorMsg: Rope.ROPE;
IF wildcardExpansion # remoteAll 
AND 
NOT HasVersion[filename] 
THEN
searchPattern ← searchPattern.Concat["!h"];
 
[matchingFiles, errorMsg] ← DirectoryList.Remote[host, searchPattern, AbortProc];
IF errorMsg.Length > 0 
THEN {
log.PutF["[%g]%g: %g\n", IO.rope[host], IO.rope[searchPattern], IO.rope[errorMsg]];
}
 
ELSE {
FOR r: 
LIST 
OF Rope.
ROPE ← matchingFiles, r.rest 
UNTIL r=
NIL 
OR UserAbort[cmd] 
DO
fullName: Rope.ROPE ← r.first;
dir, fname: Rope.ROPE;
[directory: dir, filename: fname] ← ParsePath[fullName];
IF DoOperation[host, dir, fname, log] THEN IO.SetUserAbort[cmd.in];
ENDLOOP
 
}
 
}
 
}
 
ELSE IF DoOperation[host, directory, filename, log] THEN IO.SetUserAbort[cmd.in];
};
 
ENDLOOP;
 
IF commands.EndOf THEN log.PutRope["Done."] ELSE log.PutRope["Aborted."];
};
 
DelVerCommand: Commander.CommandProc ~ {
offset: INT ← 0;
log: IO.STREAM ← cmd.out;
commands: IO.STREAM ← IO.CreateInputStreamFromRope[cmd.commandLine];
keepRope: Rope.ROPE;
keep: NAT;
Separators: 
IO.BreakProc ~ {
RETURN[
SELECT char 
FROM
IO.SP, IO.CR, IO.TAB, ',, '; => sepr,
ENDCASE => other
]};
 
keepRope ← commands.GetToken[Separators];
IF 
NOT IsNat[keepRope] 
OR (keep ← ValueOf[keepRope]) = 0 
THEN {
log.PutRope["First parameter is the number of versions to keep, and must be positive."];
RETURN;
};
 
UNTIL commands.EndOf 
OR UserAbort[cmd] 
DO
name: Rope.ROPE ~ commands.GetToken[Separators];
host, directory, filename: Rope.ROPE;
IF name.Length = 0 THEN EXIT;
[host, directory, filename] ← ParsePath[name];
IF host.Length = 0 
OR directory.Length = 0 
OR directory.Fetch[0] # '< 
THEN {
log.PutF["%g: DelVerKeep requires explicit host and top-level directory\n", IO.rope[name]];
}
 
ELSE {
pattern: Rope.ROPE ← RemoveVersionNumber[directory.Concat[filename]].Concat["*!*"];
filesToDelete: LIST OF Rope.ROPE;
errorMsg: Rope.ROPE;
AbortProc: PROC RETURNS [BOOLEAN] ~ {RETURN [UserAbort[cmd]]};
log.PutRope["Enumerating remote directory . . . "];
[filesToDelete, errorMsg] ← DirectoryList.Remote[host, pattern, AbortProc];
IF errorMsg.Length # 0 
THEN {
log.PutF["%g: %g\n", IO.rope[name], IO.rope[errorMsg]];
}
 
ELSE {
log.PutRope["done.\n"];
filesToDelete ← SortRopeList[filesToDelete];
filesToDelete ← RemoveHighestVersions[filesToDelete, keep];
IF filesToDelete = NIL THEN log.PutF["%g: No old versions to delete.\n", IO.rope[name]];
UNTIL filesToDelete = 
NIL 
OR UserAbort[cmd] 
DO
file: Rope.ROPE ← filesToDelete.first;
filesToDelete ← filesToDelete.rest;
log.PutF["Deleting [%g]%g ", IO.rope[host], IO.rope[file]];
Process.Pause[Process.MsecToTicks[pauseAmount]];
IF 
NOT UserAbort[cmd] 
THEN {
msg: Rope.ROPE ← "done.";
log.PutRope[". . . "];
TRUSTED {FtpMan.Delete[host, PutInSlashes[file] ! CIFS.Error => {msg ← error; CONTINUE}]};
log.PutRope[msg];
log.PutRope["\n"];
};
 
ENDLOOP;
 
};
 
};
 
ENDLOOP;
 
IF commands.EndOf THEN log.PutRope["Done."] ELSE log.PutRope["Aborted."];
};
 
SortRopeList: 
PROC [l: 
LIST 
OF Rope.
ROPE] 
RETURNS [
LIST 
OF Rope.
ROPE] ~ {
The sort is destructive and NOT stable, that is, the relative positions in the result of
nodes with equal keys is unpredictible.  For a nondestructive sort, copy l first.
The sort does one CONS.
a, b, mergeTo: LIST OF Rope.ROPE;
mergeToCons: LIST OF Rope.ROPE = CONS[NIL, NIL];
max: CARDINAL = 22; --number of bits in word-address space minus 2
lists: 
ARRAY [0..max) 
OF 
LIST 
OF Rope.ROPE ← 
ALL [
NIL];
lists[0] is a sorted list of length 2 or NIL, lists[1] is a sorted list of length
4 or NIL, lists[2] is a sorted list of length 8 or NIL, etc.
When Sort returns, lists = ALL [NIL] again (we do some extra work to assure this).
 
x: CARDINAL; --[0..max]
xMax: CARDINAL ← 0; --[0..max)
make each pair of consecutive elements of l into a sorted list of length 2,
then merge it into lists.
UNTIL (a ← l) = 
NIL 
OR (b ← a.rest) = 
NIL 
DO
l ← b.rest;
IF CompareFileNames[a.first, b.first] = less 
THEN {
a.rest ← b; b.rest ← NIL }
 
ELSE {
b.rest ← a; a.rest ← NIL; a ← b };
 
x ← 0;
DO
IF (b ← lists[x]) = 
NIL 
THEN {
lists[x] ← a; EXIT }
 
ELSE { 
--merge (equal length) lists a and b
lists[x] ← NIL;
mergeTo ← mergeToCons;
DO 
--assert a#NIL, b#NIL
IF CompareFileNames[a.first, b.first] = less 
THEN {
mergeTo.rest ← a;  mergeTo ← a;
IF (a ← a.rest) = NIL THEN { mergeTo.rest ← b; EXIT } }
 
ELSE { 
mergeTo.rest ← b;  mergeTo ← b;
IF (b ← b.rest) = NIL THEN { mergeTo.rest ← a; EXIT } }
 
ENDLOOP;
 
a ← mergeToCons.rest;
x ← x+1;
IF x > xMax AND (xMax ← x) = max THEN ERROR }
 
ENDLOOP;
 
ENDLOOP;
 
xMax now contains the largest x such that lists[x] # NIL.
if l's length was even, a = NIL; if l's length was odd, a = single element list.
merge a and elements of lists into result (held in a).
x ← 0;
IF a = 
NIL 
THEN {
try to make a # NIL.
UNTIL (lists[x] # NIL OR x = xMax) DO x ← x+1 ENDLOOP;
a ← lists[x];  lists[x] ← NIL;  x ← x+1 };
 
a # NIL OR x > xMax.
UNTIL x > xMax 
DO
IF (b ← lists[x]) # 
NIL 
THEN {
lists[x] ← NIL;
mergeTo ← mergeToCons;
DO
a#NIL AND b#NIL
IF CompareFileNames[a.first, b.first] = less 
THEN {
mergeTo.rest ← a;  mergeTo ← a;
IF (a ← a.rest) = NIL THEN { mergeTo.rest ← b; EXIT } }
 
ELSE { 
mergeTo.rest ← b;  mergeTo ← b;
IF (b ← b.rest) = NIL THEN { mergeTo.rest ← a; EXIT } }
 
ENDLOOP;
 
a ← mergeToCons.rest };
 
x ← x+1;
ENDLOOP;
 
RETURN [a]
};--SortRopeList
 
Reverse: 
PROC [list: 
LIST 
OF Rope.
ROPE] 
RETURNS [
LIST 
OF Rope.
ROPE] ~ {
new: LIST OF Rope.ROPE ← NIL;
WHILE list # 
NIL 
DO
current: LIST OF Rope.ROPE ← list;
list ← list.rest;
current.rest ← new;
new ← current;
ENDLOOP;
 
RETURN [new]
};
 
RemoveHighestVersions: 
PROC [files: 
LIST 
OF Rope.
ROPE, numberToKeep: 
NAT] 
RETURNS [
LIST 
OF Rope.
ROPE] ~ {
list: LIST OF Rope.ROPE ← Reverse[files];
delete: LIST OF Rope.ROPE ← NIL;
keep: LIST OF Rope.ROPE ← NIL;
n: NAT ← 0;
UNTIL list = 
NIL 
DO
current: LIST OF Rope.ROPE ← list;
list ← list.rest;
current.rest ← NIL;
IF keep = 
NIL 
OR 
NOT EqualNames[keep.first, current.first] 
THEN {
n ← 1; current.rest ← keep; keep ← current;
}
 
ELSE 
IF n < numberToKeep 
THEN {
n ← n + 1; current.rest ← keep; keep ← current;
}
 
ELSE {
current.rest ← delete; delete ← current;
};
 
ENDLOOP;
 
RETURN [delete];
};
 
UserAbort: 
PROC [cmd: Commander.Handle] 
RETURNS [
BOOLEAN] ~ {
RETURN [cmd.in.UserAbort]
};
 
Commander.Register["StoreText", StoreTextCommand, "Store the text portion of Tioga files on a remote server."];
Commander.Register["Store", StoreCommand, "Store the files onto remote servers."];
Commander.Register["Retrieve", RetrieveCommand, "Retrieve the files from remote servers."];
Commander.Register["ListRemote", ListRemoteCommand, "List files on remote servers."];
Commander.Register["ListVerbose", ListVerboseCommand, "List info for files on remote servers."];
Commander.Register["DelVerKeep", DelVerCommand, "Delete old versions of files on remote servers."];
END.