Command procedures
Delete:
FTP.ServerDeleteProc
--[h: Handle, confirm: ConfirmProc, complete: ServerCompleteProc]-- = {
code: FTP.FailureCode ← unspecified;
text: ROPE;
PerFile:
PROC [instance: Instance] = {
IF confirm[h]
THEN {
name: ROPE ;
name ← NormalizeFileName[h, local];
[] ← AlpDirectory.DeleteFile[name: name, transHandle: instance.alpTrans !
AlpDirectory.Error =>
{[code, text] ← TranslateDirectoryError[type]; GOTO deleteFailed}];
IF ~FinishTransaction[h]
THEN
SIGNAL h.Failed[transientError, "Alpine transaction aborted"];
EXITS
deleteFailed => {
[] ← FinishTransaction[h, FALSE];
SIGNAL h.Failed[code, text, TRUE];
}; -- this will be resumed
};
[] ← complete[h];
}; -- PerFile
DoFiles[h, PerFile, AlpineDirectory.lowest];
};
Enumerate:
FTP.ServerEnumerateProc
--[h: Handle, noteFile:
PROC [h: Handle]]-- =
BEGIN
PerFile:
PROC [instance: Instance] = {
noteFile[h];
};
DoFiles[h, PerFile, AlpineDirectory.all];
END;
Rename:
FTP.ServerRenameProc
--[h: Handle]-- = {
instance: Instance = GetInstance[h];
oldName: ROPE ← NormalizeFileName[h, remote];
newName: ROPE ← NormalizeFileName[h, local];
trans: AlpTransaction.Handle ← GetTransaction[h];
code: FTP.FailureCode ← unspecified;
text: ROPE;
{
[] ← AlpDirectory.Rename[old: oldName, new: newName, transHandle: trans !
AlpDirectory.Error =>
{[code, text] ← TranslateDirectoryError[type]; GOTO failed}];
IF ~FinishTransaction[h]
THEN
ERROR h.Failed[transientError, "Alpine transaction aborted"];
EXITS
failed =>
{[] ← FinishTransaction[h, FALSE]; ERROR h.Failed[code, text]};
};
};
Retrieve:
FTP.ServerRetrieveProc
--[h: Handle, confirm: ConfirmTransferProc, complete: ServerCompleteProc]-- = {
code: FTP.FailureCode ← unspecified;
text: ROPE;
PerFile:
PROC [instance: Instance] = {
stream: IO.STREAM ← confirm[h];
IF stream #
NIL
THEN {
{
PutToPupStream: ReadFileProc --[block: IO.UnsafeBlock] RETURNS [continue: BOOLEAN ← TRUE]-- = {stream.UnsafePutBlock[block]};
IF instance.alpFile =
NIL
THEN {
name: ROPE;
name ← NormalizeFileName[h, local];
[openFileID: instance.alpFile] ← AlpDirectory.OpenFile[name: name, lock: [read, fail], transHandle: instance.alpTrans !
AlpDirectory.Error =>
{[code, text] ← TranslateDirectoryError[type]; GOTO transferFailed}];
};
ReadFile[instance.alpFile, PutToPupStream !
AlpFile.LockFailed =>
{[code, text] ← TranslateLockFailure[why]; GOTO transferFailed};
AlpFile.OperationFailed =>
{[code, text] ← TranslateOperationFailure[why]; GOTO transferFailed};
AlpFile.Unknown =>
{[code, text] ← TranslateUnknownError[what]; GOTO transferFailed};
AlpFile.AccessFailed =>
{[code, text] ← TranslateAccessFailed[missingAccess]; GOTO transferFailed};
];
EXITS
transferFailed => SIGNAL h.Failed[code, text, TRUE]; -- this will be resumed
};
[] ← complete[h];
[] ← FinishTransaction[h]; -- don't care what the outcome was, since we did only reads
};
}; -- PerFile
DoFiles[h, PerFile, AlpineDirectory.highest];
};
Store:
FTP.ServerStoreProc
--[h: Handle, confirm: ConfirmTransferProc, complete: ServerCompleteProc]-- =
BEGIN
instance: Instance = GetInstance[h];
name: ROPE ← NormalizeFileName[h];
trans: AlpTransaction.Handle ← GetTransaction[h];
code: FTP.FailureCode ← unspecified;
text: ROPE;
stream: IO.STREAM;
createTime: BasicTime.GMT;
initialSize: AlpFile.PageCount;
BEGIN
ENABLE {
UNWIND => [] ← FinishTransaction[h, FALSE];
AlpDirectory.Error =>
{[code, text] ← TranslateDirectoryError[type]; GOTO commandFailed};
AlpFile.LockFailed =>
{[code, text] ← TranslateLockFailure[why]; GOTO commandFailed};
AlpFile.Unknown =>
{[code, text] ← TranslateUnknownError[what]; GOTO commandFailed};
AlpFile.AccessFailed =>
{[code, text] ← TranslateAccessFailed[missingAccess]; GOTO commandFailed};
};
IF h.GetEnumeratedProperty[type].type = unknown
THEN
ERROR h.Failed[inconsistent, "Type property required and not present"];
instance.alpFile ← NIL; -- should be NIL already
initialSize ← PagesForBytes[h.GetNumberProperty[size]];
IF initialSize=0 THEN initialSize ← defaultFileSize;
[openFileID: instance.alpFile, fullPathName: name] ← AlpDirectory.CreateFile[name: name, initialSize: initialSize, transHandle: trans !
AlpDirectory.Error => IF type = fileAlreadyExists THEN CONTINUE];
IF instance.alpFile=
NIL
THEN
-- file already exists, prepare to overwrite
[openFileID: instance.alpFile, fullPathName: name] ← AlpDirectory.OpenFile[name: name, access: readWrite, lock: [write, fail], transHandle: trans];
IF (createTime ← h.GetDateProperty[createDate]) # BasicTime.nullGMT
THEN
instance.alpFile.WriteProperties[LIST[[createTime[createTime]]]];
h.SetEnumeratedProperty[type, [type[h.GetEnumeratedProperty[type].type]]];
FillPropertyList[h, name];
stream ← confirm[h];
IF stream#
NIL
THEN
BEGIN
GetFromPupStream: WriteFileProc
--[block: IO.UnsafeBlock]
RETURNS [bytes: AlpFile.ByteCount]-- =
TRUSTED
{RETURN[stream.UnsafeGetBlock[block]]};
WriteFile[instance.alpFile, GetFromPupStream !
AlpFile.OperationFailed =>
{[code, text] ← TranslateOperationFailure[why]; GOTO transferFailed};
AlpFile.LockFailed =>
{[code, text] ← TranslateLockFailure[why]; GOTO transferFailed};
AlpFile.Unknown =>
{[code, text] ← TranslateUnknownError[what]; GOTO transferFailed};
AlpFile.AccessFailed =>
{[code, text] ← TranslateAccessFailed[missingAccess]; GOTO transferFailed};
];
IF complete[h]
AND ~FinishTransaction[h]
THEN
SIGNAL h.Failed[transientError, "Alpine transaction aborted"];
EXITS
transferFailed => {
[] ← FinishTransaction[h, FALSE];
SIGNAL h.Failed[code, text, TRUE]; -- this will be resumed
[] ← complete[h]};
END;
[] ← FinishTransaction[h, FALSE]; -- abort the transaction if not finished by now
EXITS
commandFailed =>
-- note that this is not in the scope of the
UNWIND
{[] ← FinishTransaction[h, FALSE]; ERROR h.Failed[code, text]};
END;
END;
Version:
FTP.ServerVersionProc
--[h: FTP.Handle, remoteHerald:
ROPE]
RETURNS [localHerald:
ROPE]-- =
BEGIN
RETURN [versionHerald];
END;
CheckCredentials:
FTP.CheckCredentialsProc
--[h: FTP.Handle]-- = {
name: ROPE = h.GetTextProperty[userName];
password: ROPE = h.GetTextProperty[userPassword];
code: FTP.FailureCode ← unspecified;
text: ROPE;
{
alpInstance: AlpInstance.Handle = AlpInstance.Create[fileStore: volumeGroup, caller: name, key: RPC.MakeKey[password] !
AlpInstance.Failed => {[code, text] ← TranslateAlpInstanceFailure[why]; GOTO failed}];
instance: Instance = NEW [InstanceRecord ← [alpInstance: alpInstance]];
h.SetClientData[instance];
EXITS
failed => ERROR h.Failed[code, text];
};
};
Private
Instance: TYPE = REF InstanceRecord;
InstanceRecord:
TYPE =
RECORD [
alpInstance: AlpInstance.Handle,
alpTrans: AlpTransaction.Handle ← NIL,
alpFile: AlpFile.Handle ← NIL,
transStarted: BasicTime.GMT ← BasicTime.nullGMT,
operationCount: CARDINAL ← 0];
GetInstance:
PROC [h:
FTP.Handle]
RETURNS [instance: Instance] =
INLINE
{RETURN [NARROW[h.GetClientData[]]]};
DoFiles:
PROC [h:
FTP.Handle, proc:
PROC [instance: Instance], defaultVersion: AlpDirectory.Version] =
This procedure implements the outer-level control for Delete, Enumerate, and Retrieve. It enumerates the files, fills the local property list for each one, and calls proc[instance]. It handles all errors arising from the operations it does, but proc is responsible for its own errors.
BEGIN
instance: Instance = GetInstance[h];
retriesLeft: INT ← 2;
pattern: ROPE ← NormalizeFileName[h];
name: ROPE ← NIL;
universalFile: AlpineEnvironment.UniversalFile;
firstTime: BOOLEAN ← TRUE;
code: FTP.FailureCode ← unspecified;
text: ROPE;
DO
ENABLE UNWIND => [] ← FinishTransaction[h, FALSE];
trans: AlpTransaction.Handle ← GetTransaction[h];
[fullPathName: name, file: universalFile] ← AlpDirectory.Enumerate[pattern: pattern, previousFile: name, defaultVersion: defaultVersion, transHandle: trans !
AlpDirectory.Error => {
SELECT type
FROM
transAborted => {
retriesLeft ← retriesLeft - 1;
[] ← FinishTransaction[h];
IF retriesLeft >= 0 THEN LOOP; -- start a new transaction and try again
};
fileNotFound => IF ~firstTime THEN EXIT; -- end of enumeration
ENDCASE;
[code, text] ← TranslateDirectoryError[type];
GOTO commandFailed}];
IF universalFile = AlpineEnvironment.nullUniversalFile THEN EXIT;
IF Rope.Find[name, "$$$.btree"] < 0
THEN {
FillPropertyList[h, name !
If this is the first file enumerated then allow the signal to propagate, thereby rejecting the entire command; otherwise continue with this file but pass just the name properties and not the file properties. This is not really correct, but is the best we can do given the FTP protocol as it stands.
FTP.Failed => IF ~firstTime THEN CONTINUE];
firstTime ← FALSE;
proc[instance];
};
IF instance.alpFile#
NIL
THEN {
instance.alpFile.Close[ !
The transaction may abort after all the work is done—just ignore it.
AlpFile.Unknown => CONTINUE ];
instance.alpFile ← NIL
};
REPEAT
commandFailed => {
[] ← FinishTransaction[h, FALSE];
ERROR h.Failed[code, text];
}
ENDLOOP;
[] ← FinishTransaction[h];
END;
NormalizeFileName:
PROC [h:
FTP.Handle, list:
FTP.LocalOrRemote ← remote]
RETURNS [name:
ROPE] =
Digests the file name properties (directory, nameBody, serverFileName, and version) from the specified list and returns a file name in canonical form ready to be passed to AlpineDirectory. (Note: list=local is used only for obtaining the new file name for Rename.)
Exceptions: none. If one or more of the arguments is malformed, the returned name will be malformed as well, but will be rejected by AlpineDirectory.
BEGIN
version: ROPE;
name ← h.GetTextProperty[serverFileName, list];
IF name=NIL THEN name ← h.GetTextProperty[nameBody, list];
IF name.Size[]=0
OR name.Fetch[0]#'<
THEN
BEGIN
directory: ROPE ← h.GetTextProperty[directory, list];
size: INT;
IF directory=
NIL
THEN
BEGIN
User and connect names always come from the remote property list, since for Rename these properties must be in the first (remote) list and need not be in the second. Note that the Alpine FTP server does not implement the full semantics of the "connect" operation, but rather just uses the connect name (if present) as a default directory.
directory ← h.GetTextProperty[connectName, remote];
IF directory=NIL THEN directory ← h.GetTextProperty[userName, remote];
END;
size ← directory.Size[];
name ← (IF size#0 AND directory.Fetch[0]='< AND directory.Fetch[size-1]='> THEN Rope.Concat[directory, name] ELSE Rope.Cat["<", directory, ">", name]);
END;
IF (version ← h.GetTextProperty[version, list])#
NIL
THEN
FOR i:
INT
DECREASING
IN [0..name.Size[])
DO
SELECT name.Fetch[i]
FROM
'! => EXIT;
'> => GOTO applyVersion;
NOT IN ['0..'9] => IF i < name.Size[]-1 THEN GOTO applyVersion;
ENDCASE;
REPEAT
applyVersion =>
name ← Rope.Cat[name, "!", version];
FINISHED =>
name ← Rope.Cat[name, "!", version];
ENDLOOP;
name ← Rope.Cat["[", volumeGroup, "]", name];
END;
FillPropertyList:
PROC [h:
FTP.Handle, name:
ROPE] =
Fills the local property list with all (requested) properties. File name properties are obtained by decomposing the supplied name, which should be in AlpineDirectory canonical form including version. File properties are obtained, if necessary, by opening the file with readOnly access (if not already open).
Note: if the type property has already been set in the local property list, it is not recomputed.
Exceptions: raises ERROR FTP.Failed if something goes wrong while obtaining file properties. File properties are not accessed until after the name properties have been filled in.
BEGIN
instance: Instance = GetInstance[h];
code: FTP.FailureCode ← unspecified;
text: ROPE;
desiredProperties: FTP.PropertySet ← h.GetDesiredProperties[].props;
IF desiredProperties=ALL[FALSE] THEN desiredProperties ← ALL[TRUE];
IF h.GetEnumeratedProperty[type].type # unknown THEN desiredProperties[type] ← FALSE;
IF desiredProperties[directory]
OR desiredProperties[nameBody]
OR desiredProperties[serverFileName]
OR desiredProperties[version]
THEN
BEGIN
posDir, posBody, posVer: INT ← 0;
sizeName: INT ← name.Size[];
FOR i:
INT
IN [0..sizeName)
DO
SELECT name.Fetch[i]
FROM
'< => IF posDir=0 THEN posDir ← i;
'> => posBody ← i;
'! => posVer ← i;
ENDCASE;
ENDLOOP;
IF desiredProperties[directory] THEN h.SetTextProperty[directory, name.Substr[start: posDir+1, len: posBody-posDir-1]];
IF desiredProperties[nameBody] THEN h.SetTextProperty[nameBody, name.Substr[start: posBody+1, len: posVer-posBody-1]];
IF desiredProperties[serverFileName] THEN h.SetTextProperty[serverFileName, name.Substr[start: posDir, len: sizeName-posDir]];
IF desiredProperties[version] THEN h.SetTextProperty[version, name.Substr[start: posVer+1, len: sizeName-posVer-1]];
END;
IF desiredProperties[createDate]
OR desiredProperties[size]
OR desiredProperties[type]
THEN
BEGIN
ENABLE {
AlpDirectory.Error => {[code, text] ← TranslateDirectoryError[type]; GOTO failed};
AlpFile.LockFailed => {[code, text] ← TranslateLockFailure[why]; GOTO failed};
AlpFile.Unknown => {[code, text] ← TranslateUnknownError[what]; GOTO failed};
AlpFile.AccessFailed => {[code, text] ← TranslateAccessFailed[missingAccess]; GOTO failed};
};
IF instance.alpFile=
NIL
THEN
[openFileID: instance.alpFile] ← AlpDirectory.OpenFile[name: name, lock: [read, fail], transHandle: instance.alpTrans];
IF desiredProperties[createDate]
THEN {
h.SetDateProperty[createDate, NARROW[instance.alpFile.ReadProperties[[createTime: TRUE]].first, AlpFile.PropertyValuePair.createTime].createTime];
};
IF desiredProperties[size]
THEN
h.SetNumberProperty[size, NARROW[instance.alpFile.ReadProperties[[byteLength: TRUE]].first, AlpFile.PropertyValuePair.byteLength].byteLength];
IF desiredProperties[type]
THEN
h.SetEnumeratedProperty[type, [type[DetermineFileType[instance.alpFile]]]];
EXITS
failed => ERROR h.Failed[code, text];
END;
END;
DetermineFileType:
PROC [file: AlpFile.Handle]
RETURNS [type:
FTP.Type] =
BEGIN
FileTypeProc: ReadFileProc
--[block: IO.UnsafeBlock]
RETURNS [continue:
BOOLEAN ←
TRUE]-- =
{RETURN [(type ← FTP.DataType[block]) = text]};
type ← text;
ReadFile[file, FileTypeProc];
END;
ReadFileProc: TYPE = PROC [block: IO.UnsafeBlock] RETURNS [continue: BOOLEAN ← TRUE];
ReadFile:
PROC [file: AlpFile.Handle, proc: ReadFileProc] =
Reads successive intervals of pages from the file into the stream buffer and calls proc for each one. This terminates when proc returns FALSE or the end of the file is reached. The total number of bytes passed to all the calls to proc is the minimum of the file's byteLength property and the actual size of the file in bytes.
Exceptions: any of the errors raised by AlpFile.ReadPages or ReadProperties, except that OperationFailed[nonexistentFilePage] is caught and handled internally.
BEGIN
bytesRemaining: AlpineEnvironment.ByteCount ← NARROW[file.ReadProperties[[byteLength: TRUE]].first, AlpFile.PropertyValuePair.byteLength].byteLength;
currentPage: AlpineEnvironment.PageNumber ← 0;
buffer: StreamBuffer = AllocateStreamBuffer[];
WHILE bytesRemaining>0
DO
ENABLE UNWIND => ReleaseStreamBuffer[buffer];
bytes: AlpineEnvironment.ByteCount ← MIN[bytesRemaining, buffer.pages*AlpineEnvironment.bytesPerPage];
pages: AlpFile.PageCount ← PagesForBytes[bytes];
TRUSTED {file.ReadPages[pageRun: [firstPage: currentPage, count: pages], pageBuffer:
DESCRIPTOR[buffer.buffer, pages*AlpineEnvironment.wordsPerPage] !
AlpFile.OperationFailed =>
IF why=nonexistentFilePage
THEN {
Transfer remainder of file one page at a time until it is exhausted.
IF pages>1 THEN {pages ← 1; bytes ← AlpineEnvironment.bytesPerPage; RETRY}
ELSE EXIT}]};
IF ~proc[[base: LOOPHOLE[buffer.buffer], startIndex: 0, count: bytes]] THEN EXIT;
bytesRemaining ← bytesRemaining-bytes;
currentPage ← currentPage+pages;
ENDLOOP;
ReleaseStreamBuffer[buffer];
END;
WriteFileProc: TYPE = PROC [block: IO.UnsafeBlock] RETURNS [bytes: AlpineEnvironment.ByteCount];
WriteFile:
PROC [file: AlpFile.Handle, proc: WriteFileProc] =
Repeatedly calls proc to produce sequences of bytes in the storage described by block, and writes the resulting data into the file. This terminates when proc returns less than a full block. Adjusts the file's size if necessary, and sets the byteLength property to the total number of bytes returned by all the calls to proc.
Exceptions: any of the errors raised by AlpFile.WritePages, ReadProperties, WriteProperties, or SetSize.
BEGIN
bytesWritten: AlpineEnvironment.ByteCount ← 0;
currentPage: AlpineEnvironment.PageNumber ← 0;
eofPage: AlpFile.PageCount ← file.GetSize[];
pageAdjustQuantum: AlpFile.PageCount = MAX[10, MIN[100, eofPage/4]]; -- extend file in units of 25% of its original size, bounded by [10..100] pages
buffer: StreamBuffer = AllocateStreamBuffer[];
DO
ENABLE UNWIND => ReleaseStreamBuffer[buffer];
bytes: AlpineEnvironment.ByteCount ← proc[[base: LOOPHOLE[buffer.buffer], startIndex: 0, count: buffer.pages*AlpineEnvironment.bytesPerPage]];
pages: AlpFile.PageCount;
IF bytes=0 THEN EXIT;
pages ← PagesForBytes[bytes];
IF currentPage+pages > eofPage THEN file.SetSize[(eofPage ← currentPage+MAX[pages, pageAdjustQuantum])];
TRUSTED {file.WritePages[pageRun: [firstPage: currentPage, count: pages], pageBuffer: DESCRIPTOR[buffer.buffer, pages*AlpineEnvironment.wordsPerPage]]; };
bytesWritten ← bytesWritten+bytes;
currentPage ← currentPage+pages;
IF bytes < buffer.pages*AlpineEnvironment.bytesPerPage THEN EXIT;
ENDLOOP;
ReleaseStreamBuffer[buffer];
IF currentPage+pageAdjustQuantum < eofPage THEN file.SetSize[currentPage];
file.WriteProperties[LIST[[byteLength[bytesWritten]]]];
END;
GetTransaction:
PROC [h:
FTP.Handle]
RETURNS [trans: AlpTransaction.Handle] =
Obtains and returns the transaction associated with this instance, creating a new one if necessary. This procedure may finish an existing transaction and start a new one if it has been in progress for a long time; so it should be called only in situations where no uncommitted updates are pending and no file is open.
Raises FTP.Failed[transientError] if a new transaction can't be started.
BEGIN
instance: Instance = GetInstance[h];
IF instance.alpTrans#
NIL
THEN {
IF (BasicTime.Period[from: instance.transStarted, to: BasicTime.Now[]] > maxTimePerTransaction OR instance.operationCount > maxOperationsPerTransaction) THEN
[] ← FinishTransaction[h]
ELSE RETURN[instance.alpTrans];
};
trans ← instance.alpTrans ← AlpTransaction.Create[instance.alpInstance !
AlpTransaction.OperationFailed => GOTO failed];
instance.transStarted ← BasicTime.Now[];
instance.operationCount ← 0;
EXITS
failed =>
ERROR h.Failed[transientError, "Alpine server is too busy to start another transaction"];
END;
FinishTransaction:
PROC [h:
FTP.Handle, commit:
BOOLEAN ←
TRUE]
RETURNS [committed:
BOOLEAN ←
FALSE] =
Finishes the transaction, committing it if commit=TRUE and aborting it otherwise. Returns the actual outcome. Also notes that the open file (if any) has been closed. It is not harmful to call this when there is no transaction in progress, but of course one cannot expect to influence the outcome of the transaction in that case!
BEGIN
instance: Instance = GetInstance[h];
IF instance.alpTrans#
NIL
THEN
committed ← instance.alpTrans.Finish[
IF commit
THEN commit
ELSE abort !
RPC.CallFailed => CONTINUE].outcome = commit;
instance.alpTrans ← NIL;
instance.alpFile ← NIL;
END;
PagesForBytes:
PROC [bytes: AlpineEnvironment.ByteCount]
RETURNS [pages: AlpineEnvironment.PageCount] =
INLINE
{RETURN [(bytes+AlpineEnvironment.bytesPerPage-1)/AlpineEnvironment.bytesPerPage]};
TranslateAlpInstanceFailure:
PROC [why: AlpInstance.Failure]
RETURNS [code:
FTP.FailureCode ← unspecified, text:
ROPE] =
BEGIN
c: CARDINAL = LOOPHOLE[why];
RETURN (
SELECT why
FROM
authenticateFailed => [illegalUserName, "User-name or password incorrect"],
alpineDownOrCommunications, alpineDown, grapevineDownOrCommunications => [transientError, "Cannot access Alpine server"],
ENDCASE => [unspecified, IO.PutFR["Unexplained AlpInstance.Failed[%g]", [cardinal[c]]]]);
END;
TranslateDirectoryError:
PROC [why: AlpineDirectory.ErrorType]
RETURNS [code:
FTP.FailureCode ← unspecified, text:
ROPE] = {
This translates only those errors that may reasonably be expected to occur in the Alpine FTP server. Other errors are most likely due to bugs in the server.
c: CARDINAL = LOOPHOLE[why];
RETURN (
SELECT why
FROM
damaged => [fileDataError, "Can't open: file leader page damaged"],
fileAlreadyExists => [renameDestinationExists, "Destination file already exists"],
fileNotFound => [fileNotFound, "No such file exists"],
illegalFileName => [illegalServerFileName, "Illegal file name"],
insufficientPermission => [accessDenied, "File is protected; access denied"],
lockFailed => [fileBusy, "File is busy: already open in conflicting way by some other client"],
ownerNotFound => [fileNotFound, "No such file exists"],
ownerNotFound => [illegalDirectory, "No such directory (owner)"], -- this line is correct according to the protocol, but this is not the way the IFSs work
quota => [tooLong, "Page allocation exceeded (or disk full)"],
regServersUnavailable => [accessDenied, "Access check failed due to Grapevine unavailability"],
remoteCallFailed => [transientError, "Communication breakdown with Alpine server"],
transAborted => [transientError, "Alpine transaction aborted unexpectedly"],
ENDCASE => [unspecified, IO.PutFR["Unexplained AlpDirectory.Error[%g]", [cardinal[c]]]]);
Other errors: authenticateFailed can't happen because we authenticate explicitly before trying to do anything else. ownerRecordFull can't happen because we never try to change the owner record. serverBusy and serverNotFound can't happen because they arise only from attempting to create a new transaction, which we do explicitly rather than asking AlpineDirectory to do it.
};
TranslateLockFailure:
PROC [why: AlpineEnvironment.LockFailure]
RETURNS [code:
FTP.FailureCode ← unspecified, text:
ROPE] =
BEGIN
RETURN [fileBusy, "File is busy: already open in conflicting way by some other client"];
END;
TranslateAccessFailed:
PROC [missingAccess: AlpineEnvironment.NeededAccess]
RETURNS [code:
FTP.FailureCode ← unspecified, text:
ROPE] =
BEGIN
code ← accessDenied;
text
← SELECT missingAccess
FROM
alpineWheel => "alpineWheel access needed",
fileModify => "file modify access needed",
fileRead => "file read access needed",
handleReadWrite => "handleReadWrite access needed",
ownerCreate => "owner create access needed",
ownerEntry => "ownerEntry access needed",
spaceQuota => "spaceQuota access needed (exceeded quota)",
ENDCASE => "Unexplained AlpFile.AccessFailed Error";
END;
TranslateOperationFailure:
PROC [why: AlpineEnvironment.OperationFailure]
RETURNS [code:
FTP.FailureCode ← unspecified, text:
ROPE] = {
This translates only those errors that may reasonably be expected to occur in the Alpine FTP server. Other errors are most likely due to bugs in the server.
c: CARDINAL = LOOPHOLE[why];
RETURN (
SELECT why
FROM
busy => [transientError, "Alpine server is too busy to start another transaction"],
damagedLeaderPage => [fileDataError, "Can't open: file leader page damaged"],
insufficientSpace => [tooLong, "File system is full"],
quotaExceeded => [tooLong, "Page allocation exceeded"],
regServersUnavailable => [accessDenied, "Access check failed due to Grapevine unavailability"],
ENDCASE => [unspecified, IO.PutFR["Unexplained OperationFailed[%g]", [cardinal[c]]]]);
Other errors: duplicateOwner, duplicateVolume, duplicateVolumeGroup, notAlpineWheel, ownerDatabaseFull, ownerFileFormatOrVolGroupMismatch, ownerRecordFull, ownerRecordInUse, spaceInUseByThisOwner, and totalQuotaExceeded arise only from operations we never invoke. nonexistentFilePage is always caught in any context in which it can occur and never gives rise to an FTP.Error. inconsistentDescriptor, reservedType, and unwritableProperty are programming errors.
};
TranslateUnknownError:
PROC [what: AlpineEnvironment.UnknownType]
RETURNS [code:
FTP.FailureCode ← unspecified, text:
ROPE] =
This translates only those errors that may reasonably be expected to occur in the Alpine FTP server. Other errors are most likely due to bugs in the server.
BEGIN
c: CARDINAL = LOOPHOLE[what];
RETURN (
SELECT what
FROM
openFileID, transID => [transientError, "Alpine transaction aborted unexpectedly"],
ENDCASE => [unspecified, IO.PutFR["Unexplained AlpFile.Unknown[%g]", [cardinal[c]]]]);
END;
StreamBuffer: TYPE = REF StreamBufferObject;
StreamBufferObject:
TYPE =
RECORD [
pages: AlpFile.PageCount,
buffer: LONG POINTER,
next: StreamBuffer ← NIL];
AllocateStreamBuffer:
ENTRY
PROC
RETURNS [buffer: StreamBuffer] = {
ENABLE UNWIND => NULL;
IF (buffer ← streamBufferList)#NIL THEN streamBufferList ← buffer.next
ELSE buffer ← NEW [StreamBufferObject ← [pages: streamBufferSize, buffer: VM.AddressForPageNumber[VM.Allocate[streamBufferSize].page]]];
};
ReleaseStreamBuffer:
ENTRY
PROC [buffer: StreamBuffer] = {
ENABLE UNWIND => NULL;
buffer.next ← streamBufferList;
streamBufferList ← buffer;
};
Perhaps we should establish finalization on StreamBufferObjects so that if somebody loses one the VM will be reclaimed. However, StreamBufferObjects are used only inside this module, and the code as written is careful not to lose them.
Single Packet Protocol for File Info
AlpineFileInfoInstance: AlpInstance.Handle ← NIL;
transCacheRec:
TYPE =
RECORD [
trans: AlpTransaction.Handle ← NIL,
ops: INT ← 0,
lastUse: BasicTime.GMT,
pupAddress: PupStream.PupAddress
];
SavedTrans: LIST OF transCacheRec ← NIL;
BasicTimeNow: BasicTime.GMT;
AlpineFileInfo:
FTP.FileInfoProc = {
PROC [fileName: ROPE, pupAddress: PupStream.PupAddress] RETURNS [found: BOOL, return: BOOL, version: CARDINAL, create: BasicTime.GMT, bytes: LONG CARDINAL];
instance: AlpInstance.Handle;
trans: AlpTransaction.Handle;
transRec: transCacheRec;
firstTime: BOOL ← TRUE;
IF (instance ← AlpineFileInfoInstance) =
NIL
THEN {
BindInstance[];
IF (instance ← AlpineFileInfoInstance) = NIL THEN
RETURN [FALSE, FALSE, 0, BasicTime.nullGMT, 0]};
{
FOR tries:
INT
IN [1..5]
DO
ENABLE UNWIND => IF trans # NIL THEN [] ← trans.Finish[abort !
RPC.CallFailed => CONTINUE];
longFileName: ROPE;
canonicalFileName: ROPE;
file: AlpFile.Handle;
fileID: AlpFile.FileID;
posVer: INT ← 0;
sizeName: INT;
universalFile: AlpineEnvironment.UniversalFile;
properties: LIST OF AlpineEnvironment.PropertyValuePair ;
transRec ← getTrans[instance, pupAddress, firstTime !
AlpTransaction.OperationFailed => GOTO commandFailed];
trans ← transRec.trans;
firstTime ← FALSE;
longFileName ← Rope.Cat["[", volumeGroup, "]", fileName];
[fullPathName: canonicalFileName, file: universalFile] ← AlpDirectory.Lookup[fileName: longFileName, transHandle: trans !
AlpInstance.Unknown => LOOP; -- start a new transaction and try again
RPC.CallFailed => GOTO commandFailed;
AlpDirectory.Error => {
SELECT type
FROM
transAborted => {
IF trans # NIL THEN [] ← trans.Finish[abort ! RPC.CallFailed => CONTINUE];
LOOP; -- start a new transaction and try again
};
illegalFileName, illegalPattern, entryNotFound, serverNotFound, ownerNotFound, fileNotFound, insufficientPermission =>
GOTO fileNotFound;
ENDCASE;
GOTO commandFailed;
};
];
sizeName ← canonicalFileName.InlineLength[];
FOR i:
INT
IN [0..sizeName)
DO
IF canonicalFileName.InlineFetch[i] = '! THEN {posVer ← i; EXIT};
ENDLOOP;
IF posVer > 0
THEN {
version ← Convert.CardFromRope[r: canonicalFileName.Substr[start: posVer+1, len: sizeName-posVer-1 ! RuntimeError.BoundsFault => CONTINUE] ! Convert.Error => CONTINUE];
};
IF universalFile = AlpineEnvironment.nullUniversalFile
THEN {
[] ← trans.Finish[abort ! RPC.CallFailed => CONTINUE];
RETURN [FALSE, TRUE, 0, BasicTime.nullGMT, 0]; -- file not found
};
[file, fileID] ← AlpFile.Open[transHandle: trans, universalFile: universalFile,
lock: [intendRead, fail] !
AlpInstance.Unknown => LOOP; -- start a new transaction and try again
RPC.CallFailed, AlpInstance.LockFailed => GOTO commandFailed;
AlpInstance.AccessFailed => GOTO fileNotFound;
];
properties ← AlpFile.ReadProperties[handle: file, desiredProperties: AlpFile.allProperties, lock: [read, fail] !
AlpInstance.Unknown => LOOP; -- start a new transaction and try again
RPC.CallFailed, AlpInstance.LockFailed => GOTO commandFailed;
AlpInstance.AccessFailed => GOTO fileNotFound;
];
UNTIL properties =
NIL
DO
property: AlpineEnvironment.PropertyValuePair ← properties.first ;
properties ← properties.rest;
SELECT property.property
FROM
byteLength => {
byteLength: INT ← NARROW[property, AlpineEnvironment.PropertyValuePair.byteLength].byteLength;
bytes ← byteLength;
};
createTime => {
createTime: BasicTime.GMT ← NARROW[property, AlpineEnvironment.PropertyValuePair.createTime].createTime;
create ← createTime;
};
ENDCASE;
ENDLOOP;
[] ← trans.Finish[commit ! RPC.CallFailed => CONTINUE];
putTrans[transRec, pupAddress];
RETURN [TRUE, TRUE, version, create, bytes];
ENDLOOP;
[] ← trans.Finish[abort ! RPC.CallFailed => CONTINUE];
RETURN [FALSE, FALSE, 0, BasicTime.nullGMT, 0]; -- fail if can't do it in 5 tries
EXITS
commandFailed => {
The command should be retried by the FTP client so the FTP implementation should not send an answer packet.
IF trans # NIL THEN [] ← trans.Finish[abort ! RPC.CallFailed => CONTINUE];
RETURN [FALSE, FALSE, 0, BasicTime.nullGMT, 0];
};
fileNotFound => {
The file truly does not exist (or cannot be seen by this client due to access control restrictions).
IF trans # NIL THEN [] ← trans.Finish[abort ! RPC.CallFailed => CONTINUE];
RETURN [FALSE, TRUE, 0, BasicTime.nullGMT, 0];
};
};
};
getTrans:
ENTRY
PROC [instance: AlpInstance.Handle, pupAddress: PupStream.PupAddress, firstTime:
BOOL]
RETURNS [transRec: transCacheRec] = {
ENABLE UNWIND => NULL;
IF SavedTrans = NIL OR ~firstTime THEN RETURN[[AlpTransaction.Create[instance], 0, BasicTimeNow, pupAddress]]
ELSE {
prev: LIST OF transCacheRec ← NIL;
FOR transList:
LIST
OF transCacheRec ← SavedTrans, transList.rest
UNTIL transList =
NIL
DO
saved: transCacheRec ← transList.first;
IF saved.pupAddress.net = pupAddress.net
AND saved.pupAddress.host = pupAddress.host
THEN {
IF prev = NIL THEN SavedTrans ← transList.rest ELSE prev.rest ← transList.rest;
saved.lastUse ← BasicTimeNow;
RETURN[ saved];
};
prev ← transList;
ENDLOOP;
RETURN[[AlpTransaction.Create[instance], 0, BasicTimeNow, pupAddress]];
};
};
putTrans:
ENTRY
PROC [transRec: transCacheRec, pupAddress: PupStream.PupAddress] = {
ENABLE UNWIND => NULL;
transRec.lastUse ← BasicTimeNow;
IF (transRec.ops ← transRec.ops + 1) > maxOperationsPerTransaction
THEN {
[] ← transRec.trans.Finish[abort ! RPC.CallFailed => CONTINUE];
}
ELSE SavedTrans ← CONS[transRec, SavedTrans];
};
BindInstance:
ENTRY
PROC = {
ENABLE UNWIND => NULL;
IF AlpineFileInfoInstance =
NIL
THEN {
AlpineFileInfoInstance ← AlpInstance.Create[fileStore: volumeGroup !
AlpInstance.Failed => GOTO failed;
];
};
};
CloseOldTransactions:
ENTRY
PROC = {
ENABLE UNWIND => NULL;
newSavedTrans: LIST OF transCacheRec ← NIL;
BasicTimeNow ← BasicTime.Now[];
FOR transList:
LIST
OF transCacheRec ← SavedTrans, transList.rest
UNTIL transList =
NIL
DO
IF BasicTime.Period[from: transList.first.lastUse, to: BasicTimeNow] >= maxTimePerTransaction
THEN {
[] ← transList.first.trans.Finish[abort ! RPC.CallFailed => CONTINUE];
}
ELSE {
newSavedTrans ← CONS[transList.first, newSavedTrans];
};
ENDLOOP;
SavedTrans ← newSavedTrans;
};
CloseOldAlpineFileInfoTransactions:
PROC = {
waitTime: Process.Ticks ← Process.SecondsToTicks[1];
DO
CloseOldTransactions[];
Process.Pause[waitTime];
ENDLOOP;
};