DIRECTORY
BasicTime USING [GMT, nullGMT],
FS USING [InfoProc, NameProc],
FSBackdoor USING [ErrorCode, ProduceError, Version],
FSName USING [BangStarFile, BangVersionFile, VersionFromRope],
FSRemoteFile USING [ConfirmProc, FTPTimeToGMT, Lookup, LookupResult],
FSReport USING [UnknownFile],
IO USING [STREAM],
Process USING [Detach, Seconds, SecondsToTicks],
Rope USING [Cat, Equal, Fetch, ROPE],
STP USING [Close, ConfirmProcType, Create, Delete, DesiredProperties, Enumerate, Error, ErrorCode, FileInfo, GetFileInfo, Handle, IsOpen, Login, NoteFileProcType, Open, Rename, Retrieve, SetDesiredProperties, SetDirectory, Store, ValidProperties],
UserCredentials USING [Get];
 
FSRemoteFileImpl: 
CEDAR 
MONITOR
IMPORTS FSBackdoor, FSName, FSRemoteFile, FSReport, Process, Rope, STP , UserCredentials
EXPORTS FSRemoteFile
= BEGIN 
 
Exported to FSRemoteFile
Delete: 
PUBLIC 
PROC [server, file: Rope.
ROPE, wantedCreatedTime: BasicTime.
GMT, proc: FSRemoteFile.ConfirmProc] =
BEGIN
Confirm: 
STP.ConfirmProcType 
-- [file: Rope.ROPE] RETURNS [answer: {do, skip, abort}, localStream: IO.STREAM] -- =
BEGIN
info: STP.FileInfo = STP.GetFileInfo[h];
created: BasicTime.GMT = FSRemoteFile.FTPTimeToGMT[info.create];
SELECT 
TRUE 
FROM
matchFound => answer ← abort;
NOT timeSearch 
OR wantedCreatedTime = created =>
BEGIN
answer ← IF proc [FSName.VersionFromRope[info.version]] THEN do ELSE abort;
matchFound ← TRUE;
END;
 
ENDCASE => answer ← skip;
 
END;
 
matchFound: BOOLEAN ← FALSE;
timeSearch: BOOLEAN = (wantedCreatedTime # BasicTime.nullGMT);
h: STP.Handle ← NIL;
stpCode: STP.ErrorCode;
BEGIN
ENABLE STP.Error => {stpCode ← code; GOTO ReportError};
h ← GetConnection[server];
ConditionConnection[h, file, FALSE];
STP.Delete[h, file, Confirm
! STP.Error => IF code = noSuchFile THEN CONTINUE ];
 
IF NOT matchFound
THEN 
BEGIN
IF timeSearch
THEN 
BEGIN
file ← FSName.BangStarFile[file];
STP.Delete[h, file, Confirm];
END;
 
IF NOT matchFound
THEN {stpCode ← noSuchFile; GOTO ReportError};
END;
 
EXITS ReportError =>
ReportSTPError[h, stpCode, server, file, wantedCreatedTime];
 
END;
 
ReturnConnection[server, h];
END;
 
EnumerateForInfo: 
PUBLIC 
PROC [server, pattern: Rope.
ROPE, proc: FS.InfoProc] =
BEGIN
Note: 
STP.NoteFileProcType 
-- [file: Rope.ROPE] RETURNS [continue: BOOLEAN] -- =
BEGIN
info: STP.FileInfo = STP.GetFileInfo[h];
created: BasicTime.GMT = FSRemoteFile.FTPTimeToGMT[info.create];
continue ← IF proc[MakeFullGName[server, file], NIL, created, info.size, 0] THEN yes ELSE no;
END;
 
h: STP.Handle ← NIL;
stpCode: STP.ErrorCode;
BEGIN
ENABLE 
STP.Error =>
IF code = noSuchFile
THEN CONTINUE -- ignore no match
ELSE {stpCode ← code; GOTO ReportError};
 
h ← GetConnection[server];
ConditionConnection[h, pattern, FALSE];
STP.Enumerate[h, pattern, Note];
EXITS ReportError =>
ReportSTPError[h, stpCode, server, pattern, BasicTime.nullGMT];
 
END;
 
ReturnConnection[server, h];
END;
 
EnumerateForNames: 
PUBLIC 
PROC [server, pattern: Rope.
ROPE, proc: FS.NameProc] =
BEGIN
Note: 
STP.NoteFileProcType 
-- [file: Rope.ROPE] RETURNS [continue: BOOLEAN] -- =
BEGIN
continue ← IF proc[MakeFullGName[server, file]] THEN yes ELSE no;
END;
 
h: STP.Handle ← NIL;
stpCode: STP.ErrorCode;
BEGIN
ENABLE 
STP.Error =>
IF code = noSuchFile
THEN CONTINUE -- ignore no match
ELSE {stpCode ← code; GOTO ReportError};
 
h ← GetConnection[server];
ConditionConnection[h, pattern, TRUE];
STP.Enumerate[h, pattern, Note];
EXITS ReportError =>
ReportSTPError[h, stpCode, server, pattern, BasicTime.nullGMT];
 
END;
 
ReturnConnection[server, h];
END;
 
Info: 
PUBLIC 
PROC [server, file: Rope.
ROPE, wantedCreatedTime: BasicTime.
GMT] 
RETURNS [version: FSBackdoor.Version, bytes: 
INT, created: BasicTime.
GMT] =
BEGIN
ReportError: PROC [stpCode: 
STP.ErrorCode] =
{ ReportSTPError[NIL, stpCode, server, file, wantedCreatedTime] };
 
result: FSRemoteFile.LookupResult;
[result, version, created, bytes] ← FSRemoteFile.Lookup[server, file];
SELECT result 
FROM
noResponse =>
ReportError[noNameLookupResponse];
 
noSuchFile =>
IF wantedCreatedTime = BasicTime.nullGMT
THEN ReportError[noSuchFile];
 
noSuchServer =>
ReportError[noSuchHost];
 
ok =>
IF wantedCreatedTime = BasicTime.nullGMT OR wantedCreatedTime = created
THEN RETURN;
 
ENDCASE;
 
[version, bytes, created] ← STPInfo[server, file, wantedCreatedTime];
END;
 
Rename: 
PUBLIC PROC [server, fromFile: Rope.
ROPE, fromCreated: BasicTime.
GMT, toFile: Rope.
ROPE, proc: FSRemoteFile.ConfirmProc] =
BEGIN
h: STP.Handle ← NIL;
stpCode: STP.ErrorCode;
version: FSBackdoor.Version = Info[server, fromFile, fromCreated].version;
IF proc[version]
THEN 
BEGIN
ENABLE STP.Error => {stpCode ← code; GOTO ReportError};
h ← GetConnection[server];
ConditionConnection[h, fromFile, TRUE];
fromFile ← FSName.BangVersionFile[fromFile, version];
STP.Rename[h, fromFile, toFile];
EXITS ReportError =>
ReportSTPError[h, stpCode, server, toFile, BasicTime.nullGMT];
 
END;
 
ReturnConnection[server, h];
END;
 
Retrieve: 
PUBLIC 
PROC [server, file: Rope.
ROPE, wantedCreatedTime: BasicTime.
GMT, proc: 
PROC[fullGName: Rope.
ROPE, bytes: 
INT, created: BasicTime.
GMT] 
RETURNS [
IO.
STREAM]] =
BEGIN
Confirm: 
STP.ConfirmProcType 
-- [file: Rope.ROPE] RETURNS [answer: {do, skip, abort}, localStream: IO.STREAM] -- =
BEGIN
info: STP.FileInfo = STP.GetFileInfo[h];
created: BasicTime.GMT = FSRemoteFile.FTPTimeToGMT[info.create];
SELECT 
TRUE 
FROM
matchFound => answer ← abort;
NOT timeSearch 
OR wantedCreatedTime = created =>
BEGIN
localStream ← proc[MakeFullGName[server, file], info.size, created];
answer ← IF localStream = NIL THEN abort ELSE do;
matchFound ← TRUE;
END;
 
ENDCASE => answer ← skip;
 
END;
 
matchFound: BOOLEAN ← FALSE;
timeSearch: BOOLEAN = (wantedCreatedTime # BasicTime.nullGMT);
h: STP.Handle ← NIL;
stpCode: STP.ErrorCode;
BEGIN
ENABLE STP.Error => {stpCode ← code; GOTO ReportError};
h ← GetConnection[server];
ConditionConnection[h, file, FALSE];
STP.Retrieve[h, file, Confirm
! STP.Error => IF code = noSuchFile THEN CONTINUE ];
 
IF NOT matchFound
THEN 
BEGIN
IF timeSearch
THEN 
BEGIN
file ← FSName.BangStarFile[file];
STP.Retrieve[h, file, Confirm];
END;
 
IF NOT matchFound
THEN {stpCode ← noSuchFile; GOTO ReportError}; 
END;
 
EXITS ReportError =>
ReportSTPError[h, stpCode, server, file, wantedCreatedTime];
 
END;
 
ReturnConnection[server, h];
END;
 
Store: 
PUBLIC 
PROC [server, file: Rope.
ROPE, str: 
IO.
STREAM, created: BasicTime.
GMT, proc: FSRemoteFile.ConfirmProc] =
BEGIN
Note: 
STP.NoteFileProcType 
-- [file: Rope.ROPE] RETURNS [continue: BOOLEAN] -- =
BEGIN
doIt: BOOLEAN = proc [ FSName.VersionFromRope[ STP.GetFileInfo[h].version ] ];
continue ← IF doIt THEN yes ELSE no;
END;
 
h: STP.Handle ← NIL;
stpCode: STP.ErrorCode;
BEGIN
ENABLE STP.Error => {stpCode ← code; GOTO ReportError};
h ← GetConnection[server];
ConditionConnection[h, file, TRUE];
STP.Store[h, file, str, Note, unknown, created];
EXITS ReportError =>
ReportSTPError[h, stpCode, server, file, BasicTime.nullGMT];
 
END;
 
ReturnConnection[server, h];
END;
 
 
Property stuff
MyDesiredProperties: TYPE = PACKED ARRAY STP.ValidProperties OF BoolDefaultFalse;
BoolDefaultFalse: TYPE = BOOLEAN ← FALSE;
namesOnly: MyDesiredProperties = [directory: TRUE, nameBody: TRUE, version: TRUE];
nameSizeCreated: MyDesiredProperties = [directory: TRUE, nameBody: TRUE, version: TRUE, createDate: TRUE, size: TRUE];
all: STP.DesiredProperties = ALL[TRUE]; -- stops sending of Desired-Property properties
ConditionConnection: 
PROC[h: 
STP.Handle, nameBody: Rope.
ROPE, justNames: 
BOOLEAN] =
BEGIN
IF Rope.Fetch[nameBody, 0] # '<
THEN 
BEGIN 
-- no directory, so turn off directory defaulting and Desired-Property properties
STP.SetDirectory[h, " "];
STP.SetDesiredProperties[h, all];
END
 
ELSE BEGIN
STP.SetDirectory[h, NIL];
STP.SetDesiredProperties[h, IF justNames THEN namesOnly ELSE nameSizeCreated];
END;
 
END;
 
 
STP connection cacheing
maxSlots: CARDINAL = 4; -- only need enough for all used in last TimeOut seconds
TimeOut: Process.Seconds ← 10; -- keep connection around for this long after use
Slot: 
TYPE = 
RECORD [
server: Rope.ROPE,
h: STP.Handle,
used: BOOLEAN
];
 
emptySlot: Slot = [NIL, NIL, FALSE];
opened, reused, reopened: INT ← 0; -- statistics
slot: ARRAY [0..maxSlots) OF Slot ← ALL [emptySlot];
haveSlotTimer: BOOLEAN ← FALSE;
GetConnection: 
ENTRY 
PROC[server: Rope.
ROPE] 
RETURNS [h: 
STP.Handle] =
BEGIN ENABLE UNWIND => NULL;
FOR i: 
CARDINAL 
IN [0..maxSlots) 
DO
IF slot[i] # emptySlot AND Rope.Equal[slot[i].server, server, FALSE]
THEN  
BEGIN
reused ← reused + 1;
h ← slot[i].h;
IF NOT STP.IsOpen[h]
THEN { [] ← STP.Open[h, slot[i].server]; reopened ← reopened + 1 };
slot[i] ← emptySlot;
RETURN;
END;
 
ENDLOOP;
 
BEGIN
user, password: Rope.ROPE;
h ← STP.Create[];
[user, password] ← UserCredentials.Get[];
STP.Login[h, user, password];
[] ← STP.Open[h, server];
opened ← opened + 1;
END;
 
END;
 
ReturnConnection: 
ENTRY 
PROC [server: Rope.
ROPE, h: 
STP.Handle] =
BEGIN
IF STP.IsOpen[h]
THEN FOR i: 
CARDINAL 
IN [0..maxSlots) 
DO
IF slot[i] = emptySlot
THEN BEGIN
slot[i] ← [server, h, TRUE];
IF NOT haveSlotTimer
THEN 
TRUSTED BEGIN
haveSlotTimer ← TRUE;
Process.Detach[FORK SlotTimer[]];
END;
 
RETURN;
END;
 
ENDLOOP;
 
END;
 
SlotTimer: 
ENTRY 
PROC =
BEGIN
ForTimeout: CONDITION ← [Process.SecondsToTicks[TimeOut]];
DO
noFullSlot: BOOLEAN ← TRUE;
FOR i: 
CARDINAL 
IN [0..maxSlots) 
DO
IF slot[i] # emptySlot
THEN 
BEGIN
IF slot[i].used
THEN 
BEGIN
slot[i].used ← FALSE;
noFullSlot ← FALSE;
END
 
ELSE 
BEGIN
STP.Close[slot[i].h
! STP.Error => CONTINUE ];
 
slot[i] ← emptySlot;
END;
 
END;
 
ENDLOOP;
IF noFullSlot THEN EXIT;
WAIT ForTimeout;
 
ENDLOOP;
 
haveSlotTimer ← FALSE;
END;