-- FTPAltoFile.mesa, Edit:
-- MAS Apr 21, 1980 5:37 PM
-- HGM January 27, 1981 8:09 PM
-- PLK May 22, 1980 4:38 PM
-- Copyright Xerox Corporation 1979, 1980
DIRECTORY
AltoDefs USING [BytesPerPage, BytesPerWord, PageSize],
AltoFileDefs USING [eofDA, FA, FP, LD, TIME],
DiskKDDefs USING [DiskFull],
FTPDefs,
FTPPrivateDefs,
Inline USING [BITAND, DIVMOD, LongMult],
Mopcodes USING [zPOP, zEXCH],
Process USING [Yield],
SegmentDefs USING [
AccessOptions, Append, Read, Write, VersionOptions, DefaultVersion,
OldFileOnly, FileError, FileNameError, GetEndOfFile, GetFileTimes, FileHandle,
DestroyFile, InsertFile, InsertFileLength, NewFile, ReleaseFile,
FileSegmentHandle, NewFileSegment, FileSegmentAddress, DeleteFileSegment,
SwapIn, SwapOut, Unlock, UnlockFile],
StreamDefs USING [
CreateByteStream, DiskHandle, FileLength, GetIndex, ReadBlock, SetIndex,
StreamIndex, WriteBlock, TruncateDiskStream],
String USING [AppendChar, AppendLongNumber, AppendString, EquivalentString],
Storage USING [Node, Free, PagesForWords, Pages, FreePages],
Time USING [Append, Unpack],
TimeExtra USING [PackedTimeFromString],
DirExtraDefs USING [EnumerateDirectoryMasked];
FTPAltoFile: MONITOR
-- Note: UniqueFileTag constitutes the monitor.
IMPORTS
DiskKDDefs, Inline, Process, SegmentDefs, StreamDefs, String, Storage, Time,
TimeExtra, DirExtraDefs, FTPPrivateDefs
EXPORTS FTPDefs
SHARES FTPDefs, FTPPrivateDefs =
BEGIN OPEN FTPDefs, FTPPrivateDefs;
-- **********************! Types !***********************
-- alto file system state information
AltoFileSystem: TYPE = POINTER TO AltoFileSystemObject;
AltoFileSystemObject: TYPE = RECORD [bufferSize: CARDINAL];
-- alto file handle state information
AltoFileHandle: TYPE = POINTER TO AltoFileHandleObject;
AltoFileHandleObject: TYPE = RECORD [
mode: Mode, diskHandle: StreamDefs.DiskHandle, lengthOfFile: LONG INTEGER];
-- **********************! Constants !***********************
defaultBufferSize: CARDINAL = 4*AltoDefs.PageSize;
scanByteCountBeforeYield: CARDINAL = 5*256*2;
filenameWildString: CHARACTER = '*;
filenameWildCharacter: CHARACTER = '#;
filenameNameVersionSeparator: CHARACTER = '!;
ftpsystem: POINTER TO FTPSystem = LocateFtpSystemObject[];
filePrimitivesObject: FilePrimitivesObject ←
[CreateFileSystem: CreateFileSystem, DestroyFileSystem: DestroyFileSystem,
DecomposeFilename: DecomposeFilename, ComposeFilename: ComposeFilename,
InspectCredentials: InspectCredentials, EnumerateFiles: EnumerateFiles,
OpenFile: OpenFile, ReadFile: ReadFile, WriteFile: WriteFile,
CloseFile: CloseFile, DeleteFile: DeleteFile, RenameFile: RenameFile];
-- **********************! Variables !***********************
uniqueFileTag: LONG INTEGER ← 0;
-- **********************! File Foothold Procedure !***********************
AltoFilePrimitives, SomeFilePrimitives: PUBLIC PROCEDURE
RETURNS [filePrimitives: FilePrimitives] =
BEGIN
-- return file primitives
filePrimitives ← @filePrimitivesObject;
END;
-- **********************! File Primitives !***********************
CreateFileSystem: PROCEDURE [bufferSize: CARDINAL]
RETURNS [fileSystem: FileSystem] =
BEGIN
-- Note: bufferSize expressed in pages; zero implies default.
-- local variables
altoFileSystem: AltoFileSystem;
-- allocate and initialize file system object
altoFileSystem ← Storage.Node[SIZE[AltoFileSystemObject]];
altoFileSystem↑ ← AltoFileSystemObject[
bufferSize:
IF bufferSize # 0 THEN bufferSize*AltoDefs.PageSize ELSE defaultBufferSize];
fileSystem ← LOOPHOLE[altoFileSystem];
END;
DestroyFileSystem: PROCEDURE [fileSystem: FileSystem] =
BEGIN
-- local constants
altoFileSystem: AltoFileSystem = LOOPHOLE[fileSystem];
-- release file system object
Storage.Free[altoFileSystem];
END;
DecomposeFilename: PROCEDURE [
fileSystem: FileSystem, absoluteFilename: STRING,
virtualFilename: VirtualFilename] =
BEGIN OPEN virtualFilename;
-- Note: Absolute filenames have the following syntax,
-- with name non-empty and version non-empty and numeric:
-- name [filenameNameVersionSeparator version]
-- Virtual filename components are never NIL;
-- supplies empty device and directory components.
-- local variables
i: CARDINAL;
character: CHARACTER;
field: STRING ← name;
-- initialize virtual filename components to empty
device.length ← directory.length ← name.length ← version.length ← 0;
-- process each character in absolute filename
FOR i IN [0..absoluteFilename.length) DO
-- select character
character ← absoluteFilename[i];
-- switch to version if character is name-version separator
IF field = name AND character = filenameNameVersionSeparator THEN
field ← version
-- append character to name or version as appropriate
ELSE
BEGIN
IF field = version AND character ~IN ['0..'9] THEN Abort[illegalFilename];
String.AppendChar[field, character];
END;
ENDLOOP;
-- abort if either name or version is empty
IF name.length = 0 OR (field = version AND version.length = 0) THEN
Abort[illegalFilename];
END;
ComposeFilename: PROCEDURE [
fileSystem: FileSystem, absoluteFilename: STRING,
virtualFilename: VirtualFilename] =
BEGIN OPEN virtualFilename;
-- Note: Absolute filenames have the following syntax,
-- with name non-empty and version non-empty and numeric:
-- name [filenameNameVersionSeparator version]
-- Virtual filename components are never NIL;
-- ignores device and directory components;
-- uses name and version components as defaults.
-- local constants
explicitDevice: STRING = [0];
explicitDirectory: STRING = [0];
-- local variables
explicitName: STRING ← [maxStringLength];
explicitVersion: STRING ← [maxStringLength];
explicitVirtualFilenameObject: VirtualFilenameObject ←
[device: explicitDevice, directory: explicitDirectory, name: explicitName,
version: explicitVersion];
i: CARDINAL;
-- return at once if absolute filename is all there is
IF name.length = 0 AND version.length = 0 THEN RETURN;
-- decompose absolute filename
IF absoluteFilename.length # 0 THEN
DecomposeFilename[
fileSystem, absoluteFilename, @explicitVirtualFilenameObject];
-- apply defaults as necessary
IF explicitName.length = 0 THEN explicitName ← name;
IF explicitVersion.length = 0 THEN explicitVersion ← version;
-- initialize absolute filename to empty
absoluteFilename.length ← 0;
-- output name always
IF explicitName.length = 0 THEN Abort[illegalFilename];
String.AppendString[absoluteFilename, explicitName];
-- output version if specified
IF explicitVersion.length # 0 THEN
BEGIN
-- verify that version is numeric
FOR i IN [0..explicitVersion.length) DO
IF explicitVersion[i] ~IN ['0..'9] THEN Abort[illegalFilename]; ENDLOOP;
-- output name-version separator
String.AppendChar[absoluteFilename, filenameNameVersionSeparator];
-- output version
String.AppendString[absoluteFilename, explicitVersion];
END;
END;
InspectCredentials: PROCEDURE [
fileSystem: FileSystem, status: Status, user, password: STRING] =
BEGIN
-- no operation
END;
EnumerateFiles: PROCEDURE [
fileSystem: FileSystem, files: STRING, intent: EnumerateFilesIntent,
processFile: PROCEDURE [UNSPECIFIED, STRING, FileInfo],
processFileData: UNSPECIFIED] =
BEGIN
PreProcessFile: PROCEDURE [fp: POINTER TO AltoFileDefs.FP, file: STRING]
RETURNS [BOOLEAN] =
BEGIN
-- terminating period has been flushed by EnumerateDirectoryMasked
fileHandle: SegmentDefs.FileHandle;
read, write, create: LONG CARDINAL;
page, byte: CARDINAL;
fa: AltoFileDefs.FA ← [AltoFileDefs.eofDA, 0, 0];
fileHandle ← SegmentDefs.InsertFile[fp, SegmentDefs.Read];
SegmentDefs.InsertFileLength[fileHandle, @fa];
creationDate.length ← writeDate.length ← readDate.length ← 0;
[read, write, create] ← SegmentDefs.GetFileTimes[fileHandle];
Time.Append[creationDate, Time.Unpack[create]];
Time.Append[writeDate, Time.Unpack[write]];
Time.Append[readDate, Time.Unpack[read]];
[page, byte] ← SegmentDefs.GetEndOfFile[fileHandle];
fileInfoObject.byteCount ← Inline.LongMult[page, 512] + byte - 512;
IF fileHandle.segcount = 0 THEN SegmentDefs.ReleaseFile[fileHandle];
fileInfoObject.fileType ← unknown;
fileInfoObject.byteSize ← 8;
processFile[processFileData, file, @fileInfoObject];
RETURN[FALSE];
END;
creationDate: STRING = [maxDateLength];
writeDate: STRING = [maxDateLength];
readDate: STRING = [maxDateLength];
fileInfoObject: FileInfoObject ←
[fileType:, byteSize:, byteCount:, creationDate: creationDate,
writeDate: writeDate, readDate: readDate, author: NIL];
DirExtraDefs.EnumerateDirectoryMasked[files, PreProcessFile]
END;
OpenFile: PROCEDURE [
fileSystem: FileSystem, file: STRING, mode: Mode, fileTypePlease: BOOLEAN,
info: FileInfo] RETURNS [fileHandle: FileHandle, fileType: FileType] =
BEGIN
-- Note: Supplies scratch filename if file.length=0;
-- determines file type by looking for byte with high-order bit on.
-- local constants
version: SegmentDefs.VersionOptions =
IF mode = read OR mode = readThenWrite THEN SegmentDefs.OldFileOnly
ELSE SegmentDefs.DefaultVersion;
access: SegmentDefs.AccessOptions =
SELECT mode FROM
read => SegmentDefs.Read,
write => SegmentDefs.Write + SegmentDefs.Append,
writeThenRead, readThenWrite =>
SegmentDefs.Read + SegmentDefs.Write + SegmentDefs.Append,
ENDCASE => SegmentDefs.Append; -- append
scratch: BOOLEAN = (file.length = 0);
-- local variables
altoFileHandle: AltoFileHandle;
internalFileHandle: SegmentDefs.FileHandle ← NIL;
initialStreamIndex: StreamDefs.StreamIndex;
byteCount: CARDINAL;
byte: Byte;
-- generate unique scratch filename if necessary
IF scratch THEN
BEGIN
String.AppendString[file, "FTPAltoFile-"L];
String.AppendLongNumber[file, UniqueFileTag[], 10];
String.AppendString[file, ".Scratch"L];
END;
-- allocate and initialize alto file handle object
altoFileHandle ← Storage.Node[SIZE[AltoFileHandleObject]];
altoFileHandle↑ ← AltoFileHandleObject[
mode: mode, diskHandle: NIL, lengthOfFile:];
fileHandle ← LOOPHOLE[altoFileHandle];
-- intercept errors
BEGIN OPEN altoFileHandle;
ENABLE
BEGIN
SegmentDefs.FileNameError =>
Abort[
IF version = SegmentDefs.OldFileOnly THEN noSuchFile
ELSE illegalFilename];
DiskKDDefs.DiskFull => Abort[noRoomForFile];
SegmentDefs.FileError => Abort[fileDataError];
UNWIND =>
BEGIN
IF diskHandle # NIL THEN diskHandle.destroy[diskHandle];
IF internalFileHandle # NIL THEN
SegmentDefs.ReleaseFile[internalFileHandle];
Storage.Free[altoFileHandle];
END;
END;
-- create byte stream to access file
internalFileHandle ← SegmentDefs.NewFile[file, access, version];
altoFileHandle.diskHandle ← StreamDefs.CreateByteStream[
internalFileHandle, access];
IF ~scratch AND info # NIL THEN
-- Stuff/Extract create date (and such) to/from Leader page
BEGIN -- No catch phrase to fixup leaderPage
access: SegmentDefs.AccessOptions;
leaderPage: SegmentDefs.FileSegmentHandle;
leader: POINTER TO AltoFileDefs.LD;
access ← IF mode = read THEN SegmentDefs.Read ELSE SegmentDefs.Write;
leaderPage ← SegmentDefs.NewFileSegment[internalFileHandle, 0, 1, access];
SegmentDefs.SwapIn[leaderPage];
leader ← SegmentDefs.FileSegmentAddress[leaderPage];
SELECT mode FROM
write =>
BEGIN
SetTime[info.creationDate, @leader.created];
SegmentDefs.Unlock[leaderPage];
SegmentDefs.SwapOut[leaderPage];
END;
ENDCASE =>
BEGIN
GetTime[info.creationDate, leader.created];
GetTime[info.writeDate, leader.written];
GetTime[info.readDate, leader.read];
SegmentDefs.Unlock[leaderPage];
END;
SegmentDefs.DeleteFileSegment[leaderPage];
leaderPage ← NIL;
END;
internalFileHandle ← NIL;
-- determine file type if necessary
IF fileTypePlease THEN
BEGIN
-- save initial stream position
initialStreamIndex ← StreamDefs.GetIndex[diskHandle];
-- scan file until byte with higher-order bit encountered
fileType ← text;
byteCount ← 0;
StreamDefs.SetIndex[diskHandle, [0, 0]];
UNTIL fileType = binary OR diskHandle.endof[diskHandle] DO
-- read next byte
byte ← diskHandle.get[diskHandle];
-- tally byte and yield to scheduler periodically
byteCount ← byteCount + 1;
IF byteCount >= scanByteCountBeforeYield THEN
BEGIN byteCount ← 0; Process.Yield[]; END;
-- force scan termination if byte proves file binary
IF byte > 177B THEN fileType ← binary;
ENDLOOP;
-- restore initial stream position
StreamDefs.SetIndex[diskHandle, initialStreamIndex];
END
-- leave file type unknown
ELSE fileType ← unknown;
END; -- enable
END;
Flop: PROCEDURE [AltoFileDefs.TIME] RETURNS [LONG CARDINAL] = MACHINE CODE
BEGIN Mopcodes.zEXCH; END;
Flip: PROCEDURE [LONG CARDINAL] RETURNS [AltoFileDefs.TIME] = MACHINE CODE
BEGIN Mopcodes.zEXCH; END;
GetTime: PROCEDURE [s: STRING, t: AltoFileDefs.TIME] =
BEGIN
when: LONG CARDINAL ← Flop[t];
IF s = NIL OR s.length # 0 THEN RETURN;
Time.Append[s, Time.Unpack[when]];
END;
SetTime: PROCEDURE [s: STRING, t: POINTER TO AltoFileDefs.TIME] =
BEGIN
when: LONG CARDINAL;
IF s = NIL OR s.length = 0 THEN RETURN;
when ← TimeExtra.PackedTimeFromString[s];
IF when # 0 THEN t↑ ← Flip[when];
END;
ReadFile: PROCEDURE [
fileSystem: FileSystem, fileHandle: FileHandle,
sendBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL],
sendBlockData: UNSPECIFIED] =
BEGIN
-- Note: Assumes invocation is consistent with mode declared via OpenFile;
-- no attempt is made to double-buffer because
-- the Alto file system monopolizes the processor.
-- Shorten procedure
Shorten: PROCEDURE [LONG INTEGER] RETURNS [CARDINAL] = MACHINE CODE
BEGIN Mopcodes.zPOP; END;
-- local constants
altoFileSystem: AltoFileSystem = LOOPHOLE[fileSystem];
altoFileHandle: AltoFileHandle = LOOPHOLE[fileHandle];
readFileBufferSize: CARDINAL = altoFileSystem.bufferSize;
-- local variables
streamIndex: StreamDefs.StreamIndex;
buffer: POINTER;
bufferSize: CARDINAL;
outgoingByteCount, shortLengthToGo: CARDINAL;
longLengthToGo: LONG INTEGER;
lengthOfRead: LONG INTEGER ← 0;
endOfFile: BOOLEAN ← FALSE;
-- reset stream position
BEGIN OPEN altoFileHandle;
StreamDefs.SetIndex[diskHandle, [0, 0]];
-- allocate a buffer
buffer ← Storage.Pages[Storage.PagesForWords[readFileBufferSize]];
-- process each outgoing block of file
UNTIL endOfFile DO
ENABLE UNWIND => Storage.FreePages[buffer];
-- fill buffer
longLengthToGo ← lengthOfFile - lengthOfRead;
shortLengthToGo ←
IF longLengthToGo > LAST[INTEGER] THEN LAST[INTEGER]
ELSE Shorten[longLengthToGo];
bufferSize ←
IF mode # writeThenRead THEN readFileBufferSize
ELSE MIN[
readFileBufferSize,
(shortLengthToGo + AltoDefs.BytesPerWord - 1)/AltoDefs.BytesPerWord];
outgoingByteCount ←
AltoDefs.BytesPerWord*StreamDefs.ReadBlock[
diskHandle, buffer, bufferSize];
endOfFile ←
IF mode # writeThenRead THEN diskHandle.endof[diskHandle]
ELSE lengthOfRead + outgoingByteCount >= lengthOfFile;
-- discard excess byte if any
IF endOfFile THEN
IF mode # writeThenRead THEN
BEGIN
streamIndex ← StreamDefs.FileLength[diskHandle];
IF Inline.BITAND[streamIndex.byte, 1] = 1 THEN
outgoingByteCount ← outgoingByteCount - 1;
END
ELSE
IF lengthOfRead + outgoingByteCount > lengthOfFile THEN
outgoingByteCount ← outgoingByteCount - 1;
-- empty buffer
IF outgoingByteCount > 0 THEN
sendBlock[sendBlockData, buffer, outgoingByteCount];
-- increment length of read
lengthOfRead ← lengthOfRead + outgoingByteCount;
ENDLOOP;
-- signal end of file
sendBlock[sendBlockData, buffer, 0];
-- release buffer
Storage.FreePages[buffer];
END; -- open
END;
WriteFile: PROCEDURE [
fileSystem: FileSystem, fileHandle: FileHandle,
receiveBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL] RETURNS [CARDINAL],
receiveBlockData: UNSPECIFIED] =
BEGIN
-- Note: Assumes invocation is consistent with mode declared via OpenFile;
-- no attempt is made to double-buffer because
-- the Alto file system monopolizes the processor.
-- local constants
altoFileSystem: AltoFileSystem = LOOPHOLE[fileSystem];
altoFileHandle: AltoFileHandle = LOOPHOLE[fileHandle];
writeFileBufferSize: CARDINAL = altoFileSystem.bufferSize;
-- local variables
streamIndex: StreamDefs.StreamIndex;
streamPositionEven: BOOLEAN;
buffer: POINTER;
bufferSize: CARDINAL;
endOfFile: BOOLEAN ← FALSE;
incomingByteCount, outgoingByteCount: CARDINAL;
excessByteCount, i: CARDINAL;
word: Word;
-- reset if necessary and note stream position
BEGIN OPEN altoFileHandle;
IF mode # append THEN StreamDefs.SetIndex[diskHandle, streamIndex ← [0, 0]]
ELSE streamIndex ← StreamDefs.GetIndex[diskHandle];
lengthOfFile ← streamIndex.page*AltoDefs.BytesPerPage + streamIndex.byte;
streamPositionEven ← Inline.BITAND[streamIndex.byte, 1] = 0;
-- allocate a buffer
buffer ← Storage.Pages[Storage.PagesForWords[writeFileBufferSize]];
-- process each incoming block of file
UNTIL endOfFile DO
ENABLE
BEGIN
DiskKDDefs.DiskFull => Abort[noRoomForFile];
UNWIND => Storage.FreePages[buffer];
END;
-- fill buffer
outgoingByteCount ← bufferSize ← 0;
DO
-- receive block
incomingByteCount ← receiveBlock[
receiveBlockData, buffer + bufferSize,
writeFileBufferSize - bufferSize];
endOfFile ← incomingByteCount = 0;
-- update outgoing byte count
outgoingByteCount ← outgoingByteCount + incomingByteCount;
[bufferSize, excessByteCount] ← Inline.DIVMOD[
outgoingByteCount, AltoDefs.BytesPerWord];
-- terminate input if necessary
IF endOfFile OR bufferSize = writeFileBufferSize OR excessByteCount = 1
THEN EXIT;
ENDLOOP;
-- yield to scheduler
Process.Yield[];
-- empty buffer (except possible last odd byte of file)
IF bufferSize > 0 THEN
IF streamPositionEven THEN
[] ← StreamDefs.WriteBlock[diskHandle, buffer, bufferSize]
ELSE
BEGIN
word ← buffer;
FOR i IN [0..bufferSize) DO
diskHandle.put[diskHandle, word.lhByte];
diskHandle.put[diskHandle, word.rhByte];
word ← word + 1;
ENDLOOP;
END;
-- empty buffer of last odd byte if any
IF excessByteCount = 1 THEN
BEGIN
word ← buffer + bufferSize;
diskHandle.put[diskHandle, word.lhByte];
streamPositionEven ← ~streamPositionEven;
END;
-- increment file length
lengthOfFile ← lengthOfFile + outgoingByteCount;
ENDLOOP;
-- release buffer
Storage.FreePages[buffer];
END; -- open
END;
CloseFile: PROCEDURE [
fileSystem: FileSystem, fileHandle: FileHandle, aborted: BOOLEAN] =
BEGIN
-- Note: On abort, deletes file opened for write, writeThenRead, or readThenWrite.
-- local constants
altoFileHandle: AltoFileHandle = LOOPHOLE[fileHandle];
-- local variables
fp: AltoFileDefs.FP ← altoFileHandle.diskHandle.file.fp;
internalFileHandle: SegmentDefs.FileHandle ← NIL;
-- intercept errors
BEGIN OPEN altoFileHandle;
ENABLE
BEGIN
DiskKDDefs.DiskFull => Abort[noRoomForFile];
SegmentDefs.FileError => Abort[fileDataError];
UNWIND =>
IF internalFileHandle # NIL THEN
SegmentDefs.ReleaseFile[internalFileHandle];
END;
-- destroy byte stream
StreamDefs.TruncateDiskStream[
diskHandle !
DiskKDDefs.DiskFull =>
IF aborted THEN {SegmentDefs.UnlockFile[diskHandle.file]; CONTINUE}];
-- delete file if appropriate
IF aborted AND (mode = write OR mode = writeThenRead OR mode = readThenWrite)
THEN
BEGIN
internalFileHandle ← SegmentDefs.InsertFile[@fp, SegmentDefs.Write];
SegmentDefs.DestroyFile[
internalFileHandle !
SegmentDefs.FileError =>
IF internalFileHandle.segcount # 0 OR internalFileHandle.lock # 0 THEN
CONTINUE
ELSE Abort[fileDataError]];
END;
-- release alto file handle object
Storage.Free[altoFileHandle];
END; -- enable
END;
DeleteFile: PROCEDURE [fileSystem: FileSystem, file: STRING] =
BEGIN
-- local variables
internalFileHandle: SegmentDefs.FileHandle ← NIL;
-- intercept errors
BEGIN
ENABLE
BEGIN
SegmentDefs.FileNameError => Abort[noSuchFile];
SegmentDefs.FileError => Abort[fileDataError];
UNWIND =>
IF internalFileHandle # NIL THEN
SegmentDefs.ReleaseFile[internalFileHandle];
END;
-- delete file
internalFileHandle ← SegmentDefs.NewFile[
file, SegmentDefs.Write, SegmentDefs.OldFileOnly];
SegmentDefs.DestroyFile[internalFileHandle];
END; -- enable
END;
RenameFile: PROCEDURE [fileSystem: FileSystem, currentFile, newFile: STRING] =
BEGIN
-- local variables
create: STRING = [maxDateLength];
info: FileInfoObject ← [binary, 8, 0, create, NIL, NIL, NIL];
currentFileHandle, newFileHandle, temp: FileHandle ← NIL;
-- no operation if two filenames equivalent
IF String.EquivalentString[currentFile, newFile] THEN RETURN;
-- open current and new files
[currentFileHandle, ] ← OpenFile[fileSystem, currentFile, read, FALSE, @info];
BEGIN
ENABLE
UNWIND =>
BEGIN
IF newFileHandle # NIL THEN CloseFile[fileSystem, newFileHandle, TRUE];
CloseFile[fileSystem, currentFileHandle, FALSE];
END;
[newFileHandle, ] ← OpenFile[fileSystem, newFile, write, FALSE, @info];
-- transfer contents of current file to new file
ForkTransferPair[
fileSystem, ReadFile, currentFileHandle, WriteFile, newFileHandle];
temp ← newFileHandle;
newFileHandle ← NIL;
CloseFile[fileSystem, temp, FALSE];
END; -- enable
CloseFile[fileSystem, currentFileHandle, FALSE];
DeleteFile[fileSystem, currentFile];
END;
-- **********************! Subroutines !***********************
UniqueFileTag: ENTRY PROCEDURE RETURNS [tag: LONG CARDINAL] =
BEGIN
-- generate and return unique file tag
tag ← uniqueFileTag ← uniqueFileTag + 1;
END;
-- **********************! Main Program !***********************
-- no operation
END. -- of FTPAltoFile