-- SModelImpl.Mesa
-- last edit February 25, 1983 2:18 pm by Schmidt
-- last edit April 12, 1983 12:47 pm by Paul Rovner
-- last edit May 22, 1983 1:39 pm by Russ Atkinson
 -- used
AllocateString instead of STRING ← [...] to reduce size of local frames
-- last edit June 24, 1983 5:04 pm by Doug Wyatt
 -- "PreCedar" option generalized: see ConvertToPreCedar

-- Pilot 6.0/ Mesa 7.0
-- options
-- /c alto-versions of DF stuff compatibility for Df files produced
-- /n do everything normally but DON'T store any files
-- /r ignore READONLY attribute
-- /v verify model - enumerate remote files to make sure
-- /s store back non-Readonly files even if create dates are ok
-- /t top level DF only
--
-- rules:
-- date in df file = date of local file
--  Then do nothing

-- date in df file ~= date of local file
--  If file is ReadOnly, give error message
--  Else Store the local file and update the date in the DF file

-- date in df file is omitted and file is on local disk
--  If file is ReadOnly Then fill in date and give warning
--    (don't store)
--  Else fill in date and transfer the file
--

-- verify mode:
-- if file would be transferred by above rules, go ahead and do it
--  if not, enumerate the remote server looking for the version
--  if this version of the file is not out there,
--   store it (even if there are already different versions out there)
    
DIRECTORY
  Commander: TYPE USING[CommandProc, Register],
  ConvertUnsafe: TYPE USING[ToRope],
CWF: TYPE USING [FWF1, FWF2, SetWriteProcedure, SWF1, SWF3, SWF4, WF0,
    WF1, WF2, WF3, WF4, WFC, WFCR],
  DFSubr: TYPE USING [AllocateDFSeq, DF, DFSeq, FreeDFSeq, LookupDF, NextDF,
   ParseStream, ReadInDir, WriteOut],
  Directory: TYPE USING [DeleteFile, Error, Handle, ignore, Lookup, Rename],
  File: TYPE USING[Unknown],
  FileStream: TYPE USING [GetLength, SetIndex, SetLeaderPropertiesForCapability],
FQ: TYPE USING[FileQuery, Result],
IO: TYPE USING[Handle, PutChar, PutFR, ResetUserAbort, string, UserAbort, UserAborted, PutRope, STREAM],
  IOMisc: TYPE USING[AskUser],
  LongString: TYPE USING [AppendString, EquivalentString, StringToDecimal],
  Resource: TYPE USING[Acquire, Release, AbortProc],
  Rope: TYPE USING[Fetch, ROPE, Text, Cat],
  RopeInline: TYPE USING[InlineFlatten],
  Space: TYPE USING [Create, Delete, Handle, Map, nullHandle, virtualMemory],
  UnsafeSTP: TYPE USING [Error, FileInfo, GetFileInfo, Handle,
   SetDirectory],
  STPSubr: TYPE USING [AddUserName, Connect, HandleSTPError, StopSTP, Store],
  Stream: TYPE USING [Delete, Handle, PutChar],
  Subr: TYPE USING [
  AbortMyself, AllocateString, Any, CheckForModify, CopyString, debugflg,
   EndsIn, errorflg, FileError, FreeString, GetCreateDateWithSpace, MakeTTYProcs,
   NewStream, numberofleaders, PackedTime, Prefix, PrintGreeting,
  Read, SetRemoteFilenameProp, strcpy, SubrStop, TTYProcs, Write],
  System: TYPE USING [GetClockPulses, PulsesToMicroseconds],
  Time: TYPE USING [Current],
UECP: TYPE USING[Argv, Parse],
  ViewerClasses: TYPE USING[Viewer],
  ViewerOps: TYPE USING[FindViewer, RestoreViewer];

SModelImpl: PROGRAM
IMPORTS Commander, ConvertUnsafe, CWF, DFSubr, Directory, File,
 FileStream, FQ, IO, IOMisc, LongString, Resource, Rope, RopeInline, Space, STP: UnsafeSTP,
 STPSubr, Stream, Subr, System, Time, UECP, ViewerOps = {

MAXFILE: CARDINAL = 500;

OpType: TYPE = LONG POINTER TO OpTypeRecord;
OpTypeRecord: TYPE = RECORD[
 askForConfirm: BOOLTRUE,    -- /a
 altoCompatibility: BOOLFALSE,   -- /c
 flipCameFrom: BOOLTRUE,   -- /f
-- /l is used by SDD for the librarian
 dontstorefiles: BOOLFALSE,    -- /n
 preCedar: BOOLFALSE,     -- /p
 ignorereadonly: BOOLFALSE,    -- /r
 forcestoreback: BOOLFALSE,    -- /s
 topLevelOnly: BOOLFALSE,   -- /t
 verify: BOOLFALSE,      -- /v
 quit: BOOLFALSE      -- set by typing "q" to the question
 ];

-- MDS USAGE !!!
storeSh: Stream.Handle ← NIL;
stdout: IO.Handle;
-- endof MDS USAGE

abortProc: Resource.AbortProc = TRUSTED{
--PROC [data: REF ANY] RETURNS[abort: BOOL];
in: IO.STREAM = NARROW[data, IO.STREAM];
abort ← in.UserAbort[];
IF abort THEN in.ResetUserAbort[];
};

Main: Commander.CommandProc = TRUSTED {
--PROC [cmd: Handle]
ENABLE UNWIND => [] ← Resource.Release[$SModel];
 h: Subr.TTYProcs;
 in: IO.Handle = cmd.in;
 out: IO.Handle = cmd.out;
 success: BOOLFALSE;
 otherOwner: Rope.ROPENIL;
 [success, otherOwner] ← Resource.Acquire[resource: $SModel,
  owner: "SModel",
  waitForIt: FALSE];
IF NOT success
THEN {
  out.PutRope[Rope.Cat["Waiting for ", otherOwner, " to finish..."]];
  [success, ] ← Resource.Acquire[resource: $SModel,
  owner: "SModel",
  waitForIt: TRUE,
  abortProc: abortProc,
  abortProcData: in
  ];
IF NOT success THEN {
  out.PutRope["ABORTED\n"];
RETURN;
  } ELSE out.PutRope["proceeding\n"];
  };

 h ← Subr.MakeTTYProcs[in, out, NIL, MyConfirm];
 SModelUsingProcs[h, cmd.commandLine];
 [] ← Resource.Release[$SModel];
 };

SModelUsingProcs: PROC[h: Subr.TTYProcs, commandLine: Rope.ROPE] = {
 tok: Rope.ROPE;
 flat: Rope.Text;
 opTypeRecord: OpTypeRecord ← [];
 options: OpType ← @opTypeRecord;
 starttime: Subr.PackedTime;
 p: LONG CARDINAL;
 argv: UECP.Argv ← UECP.Parse[commandLine];
  
Cleanup: PROC = {
  STPSubr.StopSTP[];
  Subr.SubrStop[];
  };
  
 {
  ENABLE {
  UNWIND => {
   h.out.ResetUserAbort[];
   Cleanup[];
   };
  STP.Error => {
   CWF.WF0["FTP Error. "L];
   IF error ~= NIL THEN
    CWF.WF2["message: %s, code %u in Stp.Mesa\n"L, error, @code];
   Subr.errorflg ← TRUE;
   GOTO leave;
   };
  Subr.AbortMyself, IO.UserAborted => {
   h.out.ResetUserAbort[];
   CWF.WF0["SModel Aborted.\n"L];
   GOTO leave;
   };
  };

 p ← System.PulsesToMicroseconds[System.GetClockPulses[]];
 storeSh ← NIL;
 Subr.errorflg ← Subr.debugflg ← FALSE;
 stdout ← h.out;
 [] ← CWF.SetWriteProcedure[MyPutChar];
 Subr.PrintGreeting["SModel"L];
 Subr.numberofleaders ← 0;
 starttime ← Time.Current[];
FOR parm: CARDINAL IN [1 .. argv.argc) DO
  tok ← argv[parm];
  IF tok.Fetch[0] = '- OR tok.Fetch[0] = '/ THEN
  SELECT tok.Fetch[1] FROM
  'a,'A => options.askForConfirm ← FALSE;
  'c,'C => options.altoCompatibility ← TRUE;
  'f,'F => options.flipCameFrom ← FALSE;
  'n,'N => options.dontstorefiles ← TRUE;
  'p,'P => options.preCedar ← options.verify ← TRUE;
  'r,'R => options.ignorereadonly ← TRUE;
  's,'S => options.forcestoreback ← TRUE; -- store back even if dates ok
  't,'T => options.topLevelOnly ← TRUE;
  'v,'V => options.verify ← TRUE;
  ENDCASE => {
   flat ← RopeInline.InlineFlatten[tok];
   CWF.WF1["Unknown flag '%s'\n"L,LOOPHOLE[flat, LONG STRING]]
   }
  ELSE {
   sdffile: LONG STRING ← Subr.AllocateString[100];
   {ENABLE UNWIND => {Subr.FreeString[sdffile]};
    flat ← RopeInline.InlineFlatten[tok];
    Subr.strcpy[sdffile, LOOPHOLE[flat]]; -- sdffile is modified later!
    TopLevelDF[sdffile, options, h];
    Subr.FreeString[sdffile]};
   };
  ENDLOOP;
EXITS
 leave => NULL;
 };
 Cleanup[];
 starttime ← Time.Current[] - starttime;
CWF.WF1["\nTotal elapsed time for SModel %lr."L,@starttime];
IF Subr.errorflg THEN CWF.WF0["\tErrors logged."L];
CWF.WFCR[];
 p ← System.PulsesToMicroseconds[System.GetClockPulses[]] - p;
 p ← p/1000;
-- CWF.WF1["\nMilliseconds: %lu\n"L, @p];
 };

TopLevelDF: PROC[sdffile: LONG STRING, options: OpType, h: Subr.TTYProcs] = {
 dfseq: DFSubr.DFSeq ← NIL;
 nstored: CARDINAL;
 df: DFSubr.DF;
 ldspace: Space.Handle ← Space.nullHandle;

Cleanup: PROC = {
  IF storeSh ~= NIL THEN Stream.Delete[storeSh];
  storeSh ← NIL;
  DFSubr.FreeDFSeq[@dfseq];
  IF ldspace ~= Space.nullHandle THEN Space.Delete[ldspace];
  ldspace ← Space.nullHandle;
  };

 {
ENABLE UNWIND => Cleanup[];
IF options.preCedar AND options.topLevelOnly THEN {
  CWF.WF0["The /p and /t options cannot be used together. Run SModel again with one or the other but not both.\n"L];
  RETURN;
  };
IF NOT Subr.Any[sdffile, '.] THEN LongString.AppendString[sdffile, ".DF"L];
IF NOT Subr.EndsIn[sdffile, ".df"L] THEN {
  CWF.WF1["Error - %s does not end in '.DF'.\n"L, sdffile];
  RETURN;
  };
-- build fake entry
 dfseq ← DFSubr.AllocateDFSeq[maxEntries: 1, zoneType: shared];
 df ← DFSubr.NextDF[dfseq];
 df.shortname ← Subr.CopyString[sdffile, dfseq.dfzone];
 df.atsign ← TRUE;
 df.presentonlocaldisk ← TRUE;
-- Subr.debugflg ← FALSE;
 ldspace ← Space.Create[size: 1, parent: Space.virtualMemory];
 Space.Map[ldspace];
IF options.verify THEN
  CWF.WF0["Checking remote files.\n"L];
 [nstored: nstored] ← RecursiveStore[dfseq, NIL, options, ldspace, h];
IF nstored > 0 AND NOT options.dontstorefiles THEN
  CWF.WF1["%u files stored.\n"L, @nstored];
 Cleanup[];
 }};

-- topdfouter points to the entry in the outer df that indirects thru this DF
-- dfseqouter is allocated and filled with its contents
-- topdfouter may be NIL
RecursiveStore: PROC[dfseqouter: DFSubr.DFSeq, topdfouter: DFSubr.DF,
  options: OpType, ldspace: Space.Handle, h: Subr.TTYProcs]
  RETURNS[outofspace: BOOL, nstored: CARDINAL,
   dffilemustbestored, alreadyPreCedar: BOOL] = {
 dfouter: DFSubr.DF;
 d, xferred, p: BOOL;
 flipped: BOOLFALSE;
 tempDFFileName: LONG STRING ← Subr.AllocateString[100];
 sold: LONG STRING ← Subr.AllocateString[100];
 {ENABLE UNWIND => {Subr.FreeString[tempDFFileName]; Subr.FreeString[sold]};
 nstored ← 0;
 dffilemustbestored ← outofspace ← FALSE;
 alreadyPreCedar ← TRUE;
 p ← FALSE;
FOR i: CARDINAL IN [0 .. dfseqouter.size) DO
  IF NOT dfseqouter[i].presentonlocaldisk THEN {
   IF NOT p THEN {
    CWF.WF1["Warning - these file(s) are not on the local disk:\n\t%s"L,
     dfseqouter[i].shortname];
    p ← TRUE;
    }
   ELSE CWF.WF1[", %s"L, dfseqouter[i].shortname];
   };
  ENDLOOP;
IF p THEN CWF.WFCR[];
 p ← FALSE;
FOR i: CARDINAL IN [0 .. dfseqouter.size) DO
  dfouter ← @dfseqouter[i];
  IF Subr.EndsIn[dfouter.shortname, ".DF"L]
  AND NOT dfouter.atsign
  AND NOT dfouter.readonly
  AND NOT LongString.EquivalentString[dfouter.shortname, topdfouter.shortname]
  THEN {
   IF NOT p THEN {
    CWF.WF2["Warning - these file(s) are not Imported nor Included in %s:\n\t%s"L,
     topdfouter.shortname, dfouter.shortname];
    p ← TRUE;
    }
   ELSE
    CWF.WF1[", %s"L, dfouter.shortname];
   };
  ENDLOOP;
IF p THEN CWF.WFCR[];
FOR i: CARDINAL IN [0 .. dfseqouter.size) DO
  dfouter ← @dfseqouter[i];
  -- see if CameFrom
  IF dfouter.cameFrom AND options.flipCameFrom THEN
   flipped ← ChangeCameFromToReleaseAs[dfouter, dfseqouter] OR flipped;
  IF options.preCedar THEN
   p ← ConvertToPreCedar[dfouter, dfseqouter];
  alreadyPreCedar ← alreadyPreCedar AND p;
  -- is this an indirect DF file?
  IF dfouter.atsign
  AND dfouter.presentonlocaldisk
  AND NOT dfouter.readonly
  AND Subr.EndsIn[dfouter.shortname, ".DF"L]
  AND (NOT options.topLevelOnly OR topdfouter = NIL) THEN {
   dfseqinner: DFSubr.DFSeq ← NIL;
   o,d, innerAlreadyPreCedar: BOOL;
   n: CARDINAL;
   sh: Stream.Handle;
   {
   ENABLE UNWIND => DFSubr.FreeDFSeq[@dfseqinner];
   IF topdfouter ~= NIL THEN
    CWF.WF1["\nNested SModel of %s.\n"L, dfouter.shortname];
   dfseqinner ← DFSubr.AllocateDFSeq[maxEntries: MAXFILE, zoneType: shared];
   sh ← Subr.NewStream[dfouter.shortname, Subr.Read
    ! Subr.FileError => GOTO err];
   DFSubr.ParseStream[sh, dfseqinner, dfouter.shortname, dfouter.using,
    FALSE, FALSE, FALSE, h];
   Stream.Delete[sh];
   IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
   DFSubr.ReadInDir[dfseqinner];
   -- at this point all the Capabilities are filled in
   [o,n,d, innerAlreadyPreCedar] ← RecursiveStore[dfseqinner, dfouter,
     options, ldspace, h];
   outofspace ← outofspace OR o;
   -- the inner dffile changed, the outer one must be stored (only if not ~= or >)
   dffilemustbestored ← dffilemustbestored OR (d AND dfouter.criterion = none);
   nstored ← nstored + n;
   IF options.verify THEN CWF.WFCR[];
   IF options.quit THEN EXIT;
   IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
   IF d OR n > 0 THEN {
    sh: Stream.Handle;
    dfinner: DFSubr.DF;
    storeDFFailed: BOOLFALSE;
    viewer: ViewerClasses.Viewer;
    
    dfouter.version ← 0;
    dfouter.createtime ← Time.Current[];
    IF NOT Subr.CheckForModify[dfouter.shortname, h] THEN
     SIGNAL Subr.AbortMyself;
    tempDFFileName.length ← 0;
    Subr.strcpy[tempDFFileName, dfouter.shortname];
    IF options.preCedar AND NOT innerAlreadyPreCedar THEN
     LongString.AppendString[tempDFFileName, "$$SModel.Temp$$"L]
    ELSE {
     sold.length ← 0;
     CWF.SWF1[sold, "%s$"L, dfouter.shortname];
     -- save the old df file
     Directory.DeleteFile[fileName: sold ! Directory.Error => CONTINUE];
     Directory.Rename[newName: sold, oldName: dfouter.shortname];
     };
    -- get the entry for the DF file
    dfinner ← DFSubr.LookupDF[dfseqinner, dfouter.shortname];
    IF dfinner ~= NIL THEN {
     dfinner.createtime ← dfouter.createtime;
     dfinner.version ← 0;
     };
    -- this writes out a new DF File, with self entry with the fake time
    sh ← Subr.NewStream[tempDFFileName, Subr.Write];
    DFSubr.WriteOut[dfseq: dfseqinner, topLevelFile: NIL,
     outputStream: sh, print: FALSE,
     altoCompatibility: options.altoCompatibility];
    Stream.Delete[sh];
    IF NOT options.preCedar OR innerAlreadyPreCedar THEN
     CWF.WF1["New file on '%s'.\n"L, dfouter.shortname];
    viewer ← ViewerOps.FindViewer[ConvertUnsafe.ToRope[tempDFFileName]];
    IF viewer ~= NIL THEN {
     IF viewer.newVersion THEN
      CWF.WF1["Warning - you are already editing %s.\n"L, tempDFFileName]
     ELSE
      ViewerOps.RestoreViewer[viewer];
     };
    -- we must make the create dates agree between the one
    -- we set in .df file and actual file we are about to store
    dfouter.cap ← Directory.Lookup[fileName: tempDFFileName,
     permissions: Directory.ignore];
    IF dfinner ~= NIL THEN -- Rename doesn't change cap's
     dfinner.cap ← dfouter.cap;
    IF NOT options.dontstorefiles
    AND NOT (dfouter.readonly AND NOT options.ignorereadonly)
    AND NOT outofspace THEN {
     -- make off by one second so if the store fails
     -- then the next SModel will store the DF file since
     -- the self-reference and create-time do not agree
     FileStream.SetLeaderPropertiesForCapability[cap: dfouter.cap,
      create: LOOPHOLE[dfouter.createtime+1]];
     IF dfouter.directory ~= NIL THEN {
      IF dfinner ~= NIL THEN {
       Subr.FreeString[dfinner.host, dfseqinner.dfzone];
       dfinner.host ← Subr.CopyString[dfouter.host, dfseqinner.dfzone];
       Subr.FreeString[dfinner.directory, dfseqinner.dfzone];
       dfinner.directory ← Subr.CopyString[dfouter.directory, dfseqinner.dfzone];
       };
      [] ← StoreBack[dfouter, dfouter.createtime, options, h
       ! STP.Error => IF code = requestRefused THEN {
       CWF.WF1["\nError - %s\n"L, error];
        GOTO ferr
        }];
      nstored ← nstored + 1;
      }
     ELSE IF dfinner ~= NIL THEN {
      [] ← StoreBack[dfinner, dfinner.createtime, options, h
       ! STP.Error => IF code = requestRefused THEN {
       CWF.WF1["\nError - %s\n"L, error];
        GOTO ferr
        }];
      nstored ← nstored + 1;
      };
     -- ELSE don't know where to store it
     EXITS
     ferr => {
      CWF.WF0["Delete extra files on remote directory and run SModel again.\n"L];
      Subr.errorflg ← TRUE;
      storeDFFailed ← TRUE;
      };
     };
    -- since (presumably) the file was stored successfully, we set the
    -- create time to the right time
    IF NOT storeDFFailed THEN
     FileStream.SetLeaderPropertiesForCapability[cap: dfouter.cap,
      create: LOOPHOLE[dfouter.createtime]];
    IF dfouter.directory ~= NIL
    AND (dfouter.readonly AND NOT options.ignorereadonly) THEN {
     CWF.WF1["Warning- you MUST STORE %s yourself since\n"L, dfouter.shortname];
     CWF.WF0[" it was ReadOnly and was not stored.\n"L];
     }
    ELSE IF options.dontstorefiles THEN {
     CWF.WF1["Warning- you MUST STORE %s yourself since\n"L, dfouter.shortname];
     CWF.WF0[" SModel /n did not store it for you.\n"L];
     };
    -- reset outer DF entry if it was > or ~=
    IF dfouter.criterion ~= none THEN {
     dfouter.createtime ← 0;
     dfouter.version ← 0;
     };
    IF options.preCedar AND NOT innerAlreadyPreCedar THEN
     Directory.DeleteFile[tempDFFileName ! Directory.Error => CONTINUE];
    DFSubr.FreeDFSeq[@dfseqinner];
    IF topdfouter ~= NIL THEN
     CWF.WF1["End of nested SModel of %s.\n\n"L, dfouter.shortname];
    LOOP; -- since we have stored it, just go around
    }
   ELSE IF NOT options.dontstorefiles THEN
    CWF.WF1["No files stored, %s not changed.\n"L, dfouter.shortname];
   IF Subr.debugflg THEN
    CWF.WF2["%u leaders read, dfseq.size = %u.\n"L,
     @Subr.numberofleaders, @dfseqinner.size];
   DFSubr.FreeDFSeq[@dfseqinner];
   IF topdfouter ~= NIL THEN
    CWF.WF1["End of nested SModel of %s.\n\n"L, dfouter.shortname];
   EXITS
   err => {
    CWF.WF1["Error - can't open '%s'\n"L, dfouter.shortname];
    Subr.errorflg ← TRUE;
    };
   }};
  IF topdfouter = NIL THEN EXIT; -- skip this test if is top level DF
  -- this is either a regular file, a ReadOnly @ DF file, or a non-ReadOnly @ DF file
  -- that we did not store in the above nested section
  [d, xferred] ← PossibleTransfer[dfouter, options,
   topdfouter.shortname, ldspace, h
   ! STP.Error => IF code = requestRefused THEN {
    CWF.WF1["\nError - %s\n"L, error];
    CWF.WF0["Only some of the files have been transferred.\n"L];
    CWF.WF0["Go and clean up your remote directories, then run SModel\n"L];
    CWF.WF0["EXACTLY as you did this time.\n"L];
    outofspace ← TRUE;
    Subr.errorflg ← TRUE;
    CONTINUE;
    }];
  IF dfouter.criterion ~= none THEN-- for the ~=, >, set version number to 0
   dfouter.version ← 0;
  dffilemustbestored ← dffilemustbestored OR d;
  IF xferred THEN nstored ← nstored + 1;
  IF outofspace OR options.quit THEN EXIT;
  ENDLOOP;
IF flipped THEN {
  CWF.WF0["\nSome CameFroms were changed to ReleaseAs clauses or deleted.\n"L];
  dffilemustbestored ← TRUE;
  };
 }; -- of ENABLE UNWIND
 Subr.FreeString[tempDFFileName];
 Subr.FreeString[sold];
 }; -- of RecursiveStore

ChangeCameFromToReleaseAs: PROC[df: DFSubr.DF, dfseq: DFSubr.DFSeq]
RETURNS[changed: BOOL] = {
IF df.cameFrom AND df.releaseHost ~= NIL THEN{
  IF df.readonly THEN {
   -- elim CameFrom on an Imports or any other ReadOnly
   Subr.FreeString[df.releaseHost, dfseq.dfzone];
   Subr.FreeString[df.releaseDirectory, dfseq.dfzone];
   df.releaseHost ← df.releaseDirectory ← NIL;
   }
  ELSE {
   t: LONG STRING;
   t ← df.releaseHost;
   df.releaseHost ← df.host;
   df.host ← t;
   t ← df.releaseDirectory;
   df.releaseDirectory ← df.directory;
   df.directory ← t;
   IF df.createtime > 0 THEN
    df.version ← 0; -- version is likely to be for releaseDirectory
   };
  df.cameFrom ← FALSE;
  changed ← TRUE;
  }
ELSE
  changed ← FALSE;
RETURN;
 };

ConvertToPreCedar: PROC[df: DFSubr.DF, dfseq: DFSubr.DFSeq]
RETURNS[alreadyPreCedar: BOOL] = {
IF df.cameFrom OR df.releaseHost = NIL THEN alreadyPreCedar ← TRUE
ELSE {
  directory: LONG STRING ← Subr.AllocateString[100];
  {ENABLE UNWIND => {Subr.FreeString[directory]};
   -- If ReleaseAs [host]<X>Y>, then make Directory [host]<PreX>Y>
   -- alreadyPreCedar if Directory is already [host]<PreX>Y>
   LongString.AppendString[directory, "Pre"L];
   LongString.AppendString[directory, df.releaseDirectory];
   alreadyPreCedar ← (LongString.EquivalentString[df.host, df.releaseHost]
    AND LongString.EquivalentString[df.directory, directory]);
   IF NOT alreadyPreCedar THEN {
    Subr.FreeString[df.host, dfseq.dfzone];
    Subr.FreeString[df.directory, dfseq.dfzone];
    df.host ← Subr.CopyString[df.releaseHost, dfseq.dfzone];
    df.directory ← Subr.CopyString[directory, dfseq.dfzone];
    };
   };
  Subr.FreeString[directory];
  };
 };

PossibleTransfer: PROC[df: DFSubr.DF, options: OpType, sdffile: LONG STRING,
  ldspace: Space.Handle, h: Subr.TTYProcs] RETURNS[dffileshouldbestored, xferred: BOOL] = {
ENABLE File.Unknown => {
  CWF.WF1["ERROR File.Unknown - problem reading %s.\n"L, df.shortname];
  GOTO problem;
  };
 havedate: LONG CARDINAL ← 0;

 xferred ← dffileshouldbestored ← FALSE;
IF options.verify THEN {
  oldCreate: LONG CARDINAL ← 0;
  found: BOOL;
  IF df.presentonlocaldisk
  AND NOT (df.readonly AND NOT options.ignorereadonly)
  AND df.criterion = none THEN {
   [create: havedate] ← Subr.GetCreateDateWithSpace[df.cap, ldspace];
   oldCreate ← df.createtime;
   df.createtime ← havedate;
   };
  found ← ProcessVerify[df, ldspace, options, h];
  IF havedate ~= 0 THEN
   df.createtime ← oldCreate;
  IF found THEN { -- if found, set create time to point to version found
   IF havedate ~= 0 THEN {
    IF df.createtime > 0 THEN
     df.createtime ← havedate -- occurs if DF file in error, but remote and local agree
    ELSE IF df.presentonlocaldisk AND df.criterion = none
    AND NOT (df.readonly AND NOT options.ignorereadonly) THEN
     df.createtime ← havedate; -- no date in DF file, but we would have stored it
    };
   RETURN;
   };
  IF NOT df.presentonlocaldisk
  OR df.criterion ~= none
  OR (df.readonly AND NOT options.ignorereadonly) THEN {
   Subr.errorflg ← TRUE;-- not found, for one of these reasons
   RETURN;
   };
  };
-- skip those files we can't/ won't xfer
IF NOT df.presentonlocaldisk OR df.criterion ~= none THEN RETURN;
IF havedate = 0 THEN
  [create: havedate] ← Subr.GetCreateDateWithSpace[df.cap, ldspace];
IF havedate = df.createtime
AND NOT options.forcestoreback
AND NOT options.verify THEN
  RETURN; -- stop if dates agree and verify found it, if /v given
IF df.createtime = 0
AND (df.readonly AND NOT options.ignorereadonly) THEN {
  CWF.WF1["Warning- %s is ReadOnly and has no create date in the DF file,\n"L,
    df.shortname];
  CWF.WF1["\t%lt has been filled in, but the file has not been transferred.\n"L,
    @havedate];
  df.createtime ← havedate;
  dffileshouldbestored ← TRUE;
  RETURN;
  };
IF (df.readonly AND NOT options.ignorereadonly) THEN {
  IF df.createtime = havedate THEN RETURN;
  -- don't change the df file for this
  CWF.WF1["Warning- %s is ReadOnly and the create dates of the file\n"L, df.shortname];
   CWF.WF0["\ton the local disk and in DF file do not agree.\n"L];
  -- dont transfer it, readonly file
  RETURN;
  };
-- actually transfer it
IF LongString.EquivalentString[df.shortname, sdffile] THEN {
  dffileshouldbestored ← TRUE; -- unless it is the self-reference
  df.version ← 0;
  }
ELSE {
  xferred ← StoreBack[df, havedate, options, h];
  IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
  };
EXITS
 problem => RETURN[FALSE, FALSE];
 };

StoreBack: PROC[df: DFSubr.DF, localCreateDate: LONG CARDINAL,
 options: OpType, h: Subr.TTYProcs] RETURNS[xferred: BOOL] = {
 filename, directory, appendfile: LONG STRINGNIL;
 nbytes: LONG CARDINAL;
 stphandle: STP.Handle;
 info: STP.FileInfo;

WFstoreSh: PROC[ch: CHAR] = {
  Stream.PutChar[storeSh, ch];
  };

 xferred ← FALSE;
IF df.host = NIL OR df.host.length = 0 THEN {
  CWF.WF1["Warning - unable to store %s on remote server.\n"L, df.shortname];
  RETURN;
  };
 filename ← Subr.AllocateString[125];
 directory ← Subr.AllocateString[100];
 appendfile ← Subr.AllocateString[100];
 {ENABLE UNWIND => {
  Subr.FreeString[filename]; Subr.FreeString[directory]; Subr.FreeString[appendfile]};
IF storeSh = NIL THEN {
  time: LONG CARDINAL;
  STPSubr.AddUserName[appendfile, "%s-smodel.files$"L, h];
  storeSh ← Subr.NewStream[appendfile, Subr.Write];
  FileStream.SetIndex[storeSh, FileStream.GetLength[storeSh]]; -- sets to EOF
  time ← Time.Current[];
  CWF.FWF1[WFstoreSh, "\n(Last run on %lt)\n"L, @time];
  IF options.verify THEN CWF.WFCR[];
  CWF.WF1["Files to be transferred are recorded on '%s'\n"L, appendfile];
  };
CWF.FWF2[WFstoreSh, "<%s>%s\n"L, df.directory, df.shortname];
IF NOT options.dontstorefiles THEN {
  IF options.askForConfirm THEN {
   ch: CHAR;
   ch ← h.Confirm[h.in, h.out, h.data,
    IO.PutFR["Store [%s]<%s>%s ", IO.string[df.host], IO.string[df.directory],
     IO.string[df.shortname]], 'y];
   IF ch = 'q OR ch = 'Q THEN {
    options.quit ← TRUE;
    GO TO return; 
    };
   IF ch = 'a THEN
    options.askForConfirm ← FALSE
   ELSE IF ch ~= 'y THEN
    GO TO return;    };
  stphandle ← STPSubr.Connect[host: df.host, onlyOne: TRUE, h: h];
  IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
  IF NOT options.askForConfirm THEN {
   IF options.verify THEN CWF.WFCR[];
   CWF.WF3["Store [%s]<%s>%s ... "L, df.host, df.directory, df.shortname];
   };
  Subr.strcpy[directory, df.directory];  -- required by MAXC
  STP.SetDirectory[stphandle, directory];
  -- remember: this Store done using DesiredProperties
  nbytes ← STPSubr.Store[stphandle: stphandle, localCap: df.cap, remoteName: df.shortname,
   createDate: localCreateDate, h: h
   ! STP.Error => IF STPSubr.HandleSTPError[stphandle, code, error, h] THEN RETRY];
  info ← STP.GetFileInfo[stphandle];
  -- CWF.WF1["%lu bytes.\n"L, @info.size]; info.size doesn't work
  IF info.version = NIL OR info.version.length = 0 THEN { -- Juniper or Twinkle
   CWF.SWF3[filename, "[%s]<%s>%s"L, df.host, info.directory, info.body];
   df.version ← 0;
   CWF.WF0["Done.\n"L];
   }
  ELSE {
   df.version ← LongString.StringToDecimal[info.version];
   CWF.SWF4[filename,
    IF Subr.Prefix[df.host, "maxc"L] THEN "[%s]<%s>%s;%s"L ELSE "[%s]<%s>%s!%s"L,
    df.host, info.directory, info.body, info.version];
   CWF.WF1["!%s.\n"L, info.version];
   };
  Subr.SetRemoteFilenameProp[df.cap, filename];
  xferred ← TRUE;
  };
 df.createtime ← localCreateDate; -- down here in case of signals
 xferred ← TRUE; -- true if dontstorefiles is true
EXITS return => {};
 }; -- of ENABLE UNWIND
 Subr.FreeString[filename]; Subr.FreeString[directory]; Subr.FreeString[appendfile];
 }; -- of StoreBack

ProcessVerify: PROC[df: DFSubr.DF, ldspace: Space.Handle, options: OpType, h: Subr.TTYProcs]
  RETURNS[found: BOOL] = {
 localdiskdate: LONG CARDINAL ← 0;
 remoteCreateTime: LONG CARDINAL ← 0;
 remoteVersion: CARDINAL ← 0;
 fres: FQ.Result;
 targetFileName: LONG STRINGNIL;
 wantExplicitVersion: BOOL ← df.criterion = none AND df.createtime = 0;
 found ← FALSE;
IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
CWF.WFC['+];
IF options.preCedar AND NOT wantExplicitVersion THEN
  df.version ← 0; -- /p will have wrong version hints
 targetFileName ← Subr.AllocateString[125];
 {ENABLE UNWIND => {Subr.FreeString[targetFileName]};
 [fres: fres, remoteVersion: remoteVersion, remoteCreateTime: remoteCreateTime]
  ← FQ.FileQuery[df.host, df.directory, df.shortname, df.version, df.createtime,
     wantExplicitVersion, h,
     targetFileName];
SELECT fres FROM
 foundCorrectVersion => {
  IF df.createtime > 0
  AND df.version > 0
  AND df.version ~= remoteVersion THEN
   CWF.WF4["%s Warning: Version !%u has date %lt, but DF file says !%u.\n"L,
     targetFileName, @remoteVersion, @remoteCreateTime, @df.version];
  found ← TRUE;
  df.version ← remoteVersion;
  IF df.presentonlocaldisk THEN
   [create: localdiskdate] ← Subr.GetCreateDateWithSpace[df.cap, ldspace];
  IF df.createtime = 0 AND df.criterion ~= none
  AND remoteCreateTime ~= localdiskdate AND localdiskdate ~= 0 THEN
   -- the >, ~= entry on local disk does not match the
   -- highest version number on remote dir
   CWF.WF3["\n%s (Warning- highest version number is dated %lt, local copy is dated %lt.) "L,
    targetFileName, @remoteCreateTime, @localdiskdate];
  };
 foundWrongVersion =>
  CWF.WF2["\n%s of %lt not found.\n"L, targetFileName, @df.createtime];
 notFound =>
  CWF.WF1["\n%s not found.\n"L, targetFileName];
ENDCASE => ERROR;
IF df.createtime = 0 AND df.criterion ~= none THEN
  df.version ← 0; -- these are confusing to users
IF df.presentonlocaldisk AND localdiskdate = 0 THEN
  [create: localdiskdate] ← Subr.GetCreateDateWithSpace[df.cap, ldspace];
IF df.presentonlocaldisk AND localdiskdate = remoteCreateTime
AND fres = foundCorrectVersion THEN
  Subr.SetRemoteFilenameProp[df.cap, targetFileName];
 };
 Subr.FreeString[targetFileName];
 };

MyConfirm: PROC[in, out: IO.Handle, data: REF ANY, msg: Rope.ROPE, dch: CHAR]
RETURNS[CHAR] = {
 value: ATOM;
 value ← IOMisc.AskUser[msg: msg, in: in, out: out,
keyList: LIST[$Yes, $No, $All, $Quit]]; -- order is important
SELECT value FROM
 $All => RETURN['a];
 $No => RETURN['n];
 $Quit => RETURN['q];
 $Yes => RETURN['y];
ENDCASE => ERROR;
 };

MyPutChar: PROC[ch: CHAR] = {
 stdout.PutChar[ch];
 };

-- start code
Commander.Register[key: "SModel", proc: Main, doc: "store a consistent and complete .df model"];
}.