-- 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