<> <> DIRECTORY BasicTime USING [GMT, nullGMT, Period], DFInternal USING [ AbortDF, CheckAbort, Client, ClientDescriptor, DefaultInteractionProc, DoAbort, GetFileInfo, LocalFileInfo, RemoteFileInfo, ReportFSError, ShortName, SimpleInteraction, YesOrNo], DFOperations USING [ AbortInteraction, DFInfoInteraction, FileAction, FileInteraction, InfoInteraction, InteractionProc, SModelAction], DFUtilities USING [ Date, DateFormat, DateToRope, DirectoryItem, FileItem, Filter, GetVersionNumber, ImportsItem, IncludeItem, ParseFromStream, ProcessItemProc, RemoveVersionNumber, SyntaxError, WriteItemToStream], FS USING [ Copy, Create, Delete, Error, GetInfo, GetName, OpenFile, OpenFileFromStream, Rename, StreamFromOpenFile, StreamOpen], FSExtras USING [NewCopy], IO USING [card, Close, GetIndex, PutFR, rope, STREAM], Rope USING [Concat, Equal, Length, ROPE]; SModelImpl: CEDAR MONITOR IMPORTS BasicTime, DFInternal, DFUtilities, FS, FSExtras, IO, Rope EXPORTS DFOperations = BEGIN OPEN Int: DFInternal, Ops: DFOperations, Utils: DFUtilities; ROPE: TYPE = Rope.ROPE; <<>> <> <<>> nSModels: NAT _ 0; <> SModel: PUBLIC PROC [ dfFile: ROPE, action: Ops.SModelAction _ [], interact: Ops.InteractionProc _ NIL, clientData: REF ANY _ NIL, log: IO.STREAM _ NIL] RETURNS [errors, warnings, filesActedUpon: INT _ 0] = { client: Int.Client = NEW[Int.ClientDescriptor _ [ (interact _ IF interact = NIL THEN Int.DefaultInteractionProc ELSE interact), clientData, log ]]; tempDF: ROPE = "SModelTemporaryDF$"; NewTemporaryDF: ENTRY PROC [pages: INT] RETURNS [out: IO.STREAM] = { outFile: FS.OpenFile; nSModels _ nSModels.SUCC; outFile _ FS.Create[ name: tempDF, pages: pages, setKeep: TRUE, keep: IF nSModels = 1 THEN 1 ELSE CARDINAL.LAST ! FS.Error => { localInfo: REF Int.LocalFileInfo _ NEW[Int.LocalFileInfo _ [name: tempDF]]; Int.ReportFSError[error, localInfo, client, $abort]; } ]; RETURN[FS.StreamFromOpenFile[outFile, $write]] }; CloseTemporaryDF: PROC [out: IO.STREAM] RETURNS [tempName: ROPE] = { outFile: FS.OpenFile = FS.OpenFileFromStream[out]; tempName _ outFile.GetName[].fullFName; -- includes version number out.Close[]; }; FinishWithTemporaryDF: ENTRY PROC [ tempName: ROPE, localName: ROPE, commit: BOOL _ TRUE] = { IF commit THEN FS.Rename[from: tempName, to: localName] ELSE FS.Delete[tempName]; nSModels _ nSModels.PRED; }; UserConfirms: PROC [localInfo: REF Int.LocalFileInfo, remoteInfo: REF Int.RemoteFileInfo] RETURNS [BOOL] = { RETURN[Int.YesOrNo[ client: client, message: IO.PutFR[ "%g {%g} to %g ?", IO.rope[localInfo.name], IO.rope[Utils.DateToRope[localInfo.date]], IO.rope[Utils.RemoveVersionNumber[remoteInfo.name]], ], default: TRUE ]] }; SModelInner: PROC [dfFile: ROPE, date: Utils.Date] RETURNS [REF Int.RemoteFileInfo] = { directoryPath: ROPE _ NIL; somethingChanged: BOOL _ FALSE; dfRemoteInfo: REF Int.RemoteFileInfo = NEW[Int.RemoteFileInfo _ [name: dfFile, date: date]]; dfLocalInfo: REF Int.LocalFileInfo = NEW[Int.LocalFileInfo _ [name: Int.ShortName[dfFile]]]; DoOneItem: Utils.ProcessItemProc = { StoreThisFile: PROC [ localInfo: REF Int.LocalFileInfo, remoteInfo: REF Int.RemoteFileInfo, formatFromDF: Utils.DateFormat] RETURNS [store: BOOL] = { <> lGMT: BasicTime.GMT = localInfo.date.gmt; -- can't be nullGMT rGMT: BasicTime.GMT = remoteInfo.date.gmt; -- may be nullGMT <> IF localInfo.attachedTo.Length[] = 0 THEN RETURN[TRUE]; <> IF ~Rope.Equal[ Utils.RemoveVersionNumber[remoteInfo.name], Utils.RemoveVersionNumber[localInfo.attachedTo], FALSE] THEN { <> IF remoteInfo.date = localInfo.date AND Rope.Equal[ Int.ShortName[remoteInfo.name], Int.ShortName[localInfo.attachedTo], FALSE] THEN { FS.Copy[ to: Utils.RemoveVersionNumber[localInfo.name], from: remoteInfo.name, wantedCreatedTime: remoteInfo.date.gmt, remoteCheck: action.remoteCheck, attach: TRUE ! FS.Error => GO TO storeAnyway ]; Int.GetFileInfo[localInfo -- update attachedTo ! FS.Error => Int.ReportFSError[error, localInfo, client, $abort] ]; RETURN[FALSE]; EXITS storeAnyway => NULL; }; RETURN[TRUE] }; <> SELECT formatFromDF FROM $explicit, $omitted => RETURN[lGMT ~= rGMT]; $notEqual => RETURN[action.remoteCheck AND lGMT ~= rGMT]; $greaterThan => SELECT TRUE FROM ~action.remoteCheck => RETURN[FALSE]; remoteInfo.date.format ~= $explicit => RETURN[TRUE]; -- no extant remote file ENDCASE => SELECT BasicTime.Period[from: lGMT, to: rGMT] FROM < 0 => RETURN[TRUE]; = 0 => RETURN[FALSE]; > 0 => { warnings _ warnings.SUCC; Int.SimpleInteraction[ client, NEW[Ops.InfoInteraction _ [ class: $warning, message: IO.PutFR["%g {%g} is referenced with '>' in the DF file but is older than the remote version %g {%g}.", IO.rope[localInfo.name], IO.rope[Utils.DateToRope[localInfo.date]], IO.rope[remoteInfo.name], IO.rope[Utils.DateToRope[remoteInfo.date]] ] ]] ]; RETURN[FALSE] }; ENDCASE; ENDCASE; }; ReportMissing: PROC [localInfo: REF Int.LocalFileInfo] = { warnings _ warnings.SUCC; Int.SimpleInteraction[ client, NEW[Ops.InfoInteraction _ [ class: $warning, message: IO.PutFR["'%g' doesn't exist.", IO.rope[localInfo.name]] ]] ]; }; SelfReference: PROC [shortName: ROPE] RETURNS [BOOL] = { RETURN[shortName.Equal[dfLocalInfo.name, FALSE]] }; Int.CheckAbort[client]; WITH item SELECT FROM directory: REF Utils.DirectoryItem => directoryPath _ directory.path1; file: REF Utils.FileItem => { shortName: ROPE = Utils.RemoveVersionNumber[file.name]; localInfo: REF Int.LocalFileInfo _ NEW[Int.LocalFileInfo _ [shortName]]; remoteInfo: REF Int.RemoteFileInfo _ NEW[Int.RemoteFileInfo _ [ name: directoryPath.Concat[file.name], date: file.date ]]; Int.GetFileInfo[info: localInfo, remoteCheck: TRUE ! FS.Error => IF error.group = $user THEN {ReportMissing[localInfo]; GO TO skip} ELSE Int.ReportFSError[error, localInfo, client, $abort] ]; IF action.remoteCheck THEN { <> remoteInfo.date _ localInfo.date; Int.GetFileInfo[info: remoteInfo, notFoundOK: TRUE, client: client, errorLevel: $abort]; }; remoteInfo.name _ Utils.RemoveVersionNumber[remoteInfo.name]; <> SELECT TRUE FROM SelfReference[shortName] => { <> remoteDFInfo: REF Int.RemoteFileInfo = NEW[Int.RemoteFileInfo _ [name: remoteInfo.name]]; Int.GetFileInfo[ info: remoteDFInfo, notFoundOK: TRUE, client: client, errorLevel: $abort]; IF remoteDFInfo.date.gmt ~= BasicTime.nullGMT AND file.date.format = $explicit AND BasicTime.Period[from: file.date.gmt, to: remoteDFInfo.date.gmt] > 0 AND ~Int.YesOrNo[ client: client, message: IO.PutFR[ "%g may be obsolete, since the remote version from which it is derived {%g} is older than %g {%g} (the present newest remote one); continue anyway?", IO.rope[shortName], IO.rope[Utils.DateToRope[file.date]], IO.rope[remoteDFInfo.name], IO.rope[Utils.DateToRope[remoteDFInfo.date]] ], default: FALSE, blunder: TRUE ] THEN Int.DoAbort[client.log]; IF StoreThisFile[localInfo, remoteInfo, file.date.format] THEN somethingChanged _ TRUE; -- we'll actually store it later, if user approves. dfRemoteInfo.name _ remoteInfo.name; -- redundant unless top-level DF file.name _ shortName; -- ensure no version number SELECT file.date.format FROM $explicit, $omitted => file.date _ [$explicit, outFile.GetInfo[].created]; ENDCASE; }; StoreThisFile[localInfo, remoteInfo, file.date.format] AND UserConfirms[localInfo, remoteInfo] => { somethingChanged _ TRUE; IF action.storeChanged THEN { Int.SimpleInteraction[ client, NEW[Ops.FileInteraction _ [ localFile: localInfo.name, remoteFile: remoteInfo.name, dateFormat: SELECT file.date.format FROM $explicit => $explicit, $greaterThan => $greaterThan, ENDCASE => $notEqual, date: localInfo.date.gmt, action: store ]] ]; file.name _ Int.ShortName[ file: FSExtras.NewCopy[ to: remoteInfo.name, from: localInfo.name, attach: TRUE ! FS.Error => Int.ReportFSError[error, remoteInfo, client, $abort]], keepVersion: TRUE ]; }; filesActedUpon _ filesActedUpon.SUCC; SELECT file.date.format FROM $explicit, $omitted => file.date _ localInfo.date; ENDCASE => file.name _ shortName; }; ENDCASE => <> IF localInfo.attachedTo.Length ~= 0 THEN { remoteVersion: ROPE = Utils.GetVersionNumber[localInfo.attachedTo]; IF ~remoteVersion.Equal[Utils.GetVersionNumber[file.name]] THEN { file.name _ shortName.Concat[remoteVersion]; somethingChanged _ TRUE; }; SELECT file.date.format FROM $omitted => { file.date _ localInfo.date; somethingChanged _ TRUE; }; $explicit => IF file.date ~= localInfo.date THEN { file.date _ localInfo.date; somethingChanged _ TRUE; }; ENDCASE; }; EXITS skip => NULL; }; include: REF Utils.IncludeItem => { remoteInfo: REF Int.RemoteFileInfo = SModelInner[include.path1, include.date]; IF ~Rope.Equal[remoteInfo.name, include.path1, FALSE] THEN { include.path1 _ remoteInfo.name; somethingChanged _ TRUE}; SELECT include.date.format FROM $explicit, $omitted => {include.date _ remoteInfo.date; somethingChanged _ TRUE}; ENDCASE => include.path1 _ Utils.RemoveVersionNumber[include.path1]; }; imports: REF Utils.ImportsItem => { localInfo: REF Int.LocalFileInfo = NEW[Int.LocalFileInfo _ [name: Int.ShortName[imports.path1]]]; remoteInfo: REF Int.RemoteFileInfo = NEW[Int.RemoteFileInfo _ [name: imports.path1, date: imports.date]]; Int.GetFileInfo[info: localInfo, remoteCheck: TRUE ! FS.Error => IF error.group = $user THEN {ReportMissing[localInfo]; GO TO skip} ELSE Int.ReportFSError[error, localInfo, client, $abort] ]; IF action.remoteCheck THEN { Int.GetFileInfo[info: remoteInfo, notFoundOK: TRUE, client: client, errorLevel: $abort]; IF StoreThisFile[localInfo, remoteInfo, imports.date.format] THEN { warnings _ warnings.SUCC; Int.SimpleInteraction[ client, NEW[Ops.InfoInteraction _ [ class: $warning, message: IO.PutFR["%g {%g} doesn't correspond to %g {%g}.", IO.rope[localInfo.name], IO.rope[Utils.DateToRope[localInfo.date]], IO.rope[remoteInfo.name], IO.rope[Utils.DateToRope[remoteInfo.date]] ] ]] ]; }; imports.path1 _ remoteInfo.name; }; SELECT imports.date.format FROM $explicit => NULL; $omitted => {imports.date _ localInfo.date; somethingChanged _ TRUE}; ENDCASE => imports.path1 _ Utils.RemoveVersionNumber[imports.path1]; EXITS skip => NULL; }; ENDCASE; Utils.WriteItemToStream[out, item]; }; in: IO.STREAM = FS.StreamOpen[dfLocalInfo.name, $read ! FS.Error => Int.ReportFSError[error, dfLocalInfo, client, $abort] ]; inFile: FS.OpenFile = FS.OpenFileFromStream[in]; out: IO.STREAM = NewTemporaryDF[inFile.GetInfo[].pages]; outFile: FS.OpenFile = FS.OpenFileFromStream[out]; dfLocalInfo.date _ [$explicit, inFile.GetInfo[].created]; Int.SimpleInteraction[ client, NEW[Ops.DFInfoInteraction _ [action: start, dfFile: dfLocalInfo.name]] ]; Utils.ParseFromStream[in, DoOneItem, Utils.Filter[comments: TRUE] ! Utils.SyntaxError -- [reason: ROPE]-- => Int.DoAbort[ log, IO.PutFR["Syntax error in '%g'[%d]: %g", IO.rope[dfLocalInfo.name], IO.card[in.GetIndex[]], IO.rope[reason] ], interact, clientData ]; Int.AbortDF => { in.Close[]; FinishWithTemporaryDF[CloseTemporaryDF[out], NIL, FALSE]; }; ]; in.Close[]; dfLocalInfo.date.gmt _ outFile.GetInfo[].created; IF somethingChanged AND UserConfirms[dfLocalInfo, dfRemoteInfo] THEN { tempName: ROPE = CloseTemporaryDF[out]; dfRemoteDateFormat: Utils.DateFormat = dfRemoteInfo.date.format; dfRemoteInfo.date _ dfLocalInfo.date; IF action.storeChanged THEN { Int.SimpleInteraction[ client, NEW[Ops.FileInteraction _ [ localFile: dfLocalInfo.name, remoteFile: dfRemoteInfo.name, dateFormat: SELECT dfRemoteDateFormat FROM $explicit => $explicit, $greaterThan => $greaterThan, ENDCASE => $notEqual, date: dfLocalInfo.date.gmt, action: $store ]] ]; [] _ FSExtras.NewCopy[to: dfRemoteInfo.name, from: tempName, attach: TRUE ! FS.Error => Int.ReportFSError[error, dfRemoteInfo, client, $abort]]; FinishWithTemporaryDF[tempName, dfLocalInfo.name]; }; filesActedUpon _ filesActedUpon.SUCC; } ELSE FinishWithTemporaryDF[CloseTemporaryDF[out], NIL, FALSE]; Int.SimpleInteraction[ client, NEW[Ops.DFInfoInteraction _ [ action: end, dfFile: dfLocalInfo.name, message: IF somethingChanged THEN NIL ELSE "DF file is unchanged" ]] ]; RETURN[dfRemoteInfo] }; [] _ SModelInner[dfFile, [] ! ABORTED => { Int.SimpleInteraction[client, NEW[Ops.AbortInteraction _ [TRUE]]]; <> }; Int.AbortDF => { errors _ errors.SUCC; Int.SimpleInteraction[client, NEW[Ops.DFInfoInteraction _ [action: $abort, dfFile: dfFile]]]; CONTINUE }; ]; }; END.