-- AlpineInterimBackupImpl.mesa
-- Last Edited by
--   MBrown on July 22, 1983 4:30 pm
--   Kolling on July 29, 1983 4:57 pm

  DIRECTORY
    AlpineCmds,
    CIFS,
    FileIO,
    FileLookup,
    FtpMan,
    GVBasics,
    GVSend,
    IO,
    MiscServerAdmin,
    MiscServerRemote,
    Process,
    Rope,
    System,
    Time,
    UserCredentials,
    UserExec;

AlpineInterimBackupImpl: CEDAR MONITOR
  IMPORTS
    AlpineCmds,
    CIFS,
    FileIO,
    FileLookup,
    FtpMan,
    GVSend,
    IO,
    MiscServerAdmin,
    Process,
    Rope,
    System,
    Time,
    UserCredentials,
    UserExec
  = BEGIN
  ROPE: TYPE = Rope.ROPE;
  STREAM: TYPE = IO.STREAM;

  BackupProcess: PROC [when: [0..24)] = TRUSTED {
    lastBackupDay: [0 .. 31] ← 0;
    unpacked: Time.Unpacked;
    DO
      unpacked ← Time.Unpack[System.GetGreenwichMeanTime[]];
      IF unpacked.hour = when AND unpacked.day # lastBackupDay THEN {
        [] ← ForkAlpineBackup[alpineFiles: NIL, messagesTo: NIL];
        lastBackupDay ← unpacked.day;
        };
      Process.Pause[Process.SecondsToTicks[60]];
      ENDLOOP;
    };

  StartAlpineBackup: PROC [
    command: MiscServerRemote.CommandRec, data: REF ANY] RETURNS [ROPE] = {
    RETURN [ForkAlpineBackup[
      alpineFiles: command.arg.first,
      messagesTo: command.arg.rest.first]];
    };

  backupInProgress: BOOL ← FALSE;
  StartBackup: ENTRY PROC [] RETURNS [alreadyInProgress: BOOL] = {
    alreadyInProgress ← backupInProgress;
    backupInProgress ← TRUE;
    RETURN [alreadyInProgress];
    };
  EndBackup: ENTRY PROC [] = {
    backupInProgress ← FALSE;
    };

  ForkAlpineBackup: PROC [alpineFiles: ROPE, messagesTo: ROPE]
    RETURNS [ROPE] = TRUSTED {
    IF alpineFiles.IsEmpty[] THEN
      alpineFiles ← "[indigo]<Alpine>Backup>AlpineFilesToBackup.txt↑";
    IF messagesTo.IsEmpty[] THEN messagesTo ← "Kolling.pa";
    IF StartBackup[].alreadyInProgress THEN RETURN ["Backup already in progress!"];
    Process.Detach[FORK DoBackup[alpineFiles, "Ivy", "AlpineBackup", messagesTo]];
    RETURN [NIL];
    };

  DoBackup: PROC [alpineFiles: ROPE, server, directory: ROPE, messagesTo: ROPE] = {
    -- alpineFiles is either "<full path name>↑", meaning contents of the named file, or
    -- "<full path name> <full path name> ... <full path name>", meaning itself.
    -- if a full path name is prefixed with '= then file is backed up even if create dates agree
    -- server is of form "Ivy"
    -- directory is of form "AlpineBackup"
    -- messagesTo is of form "RWeaver.pa, Kolling.pa, MBrown.pa"
    in, err: STREAM;
    toList: LIST OF RName ← NIL;
    counts: ARRAY BackupFileInfo OF INT ← ALL[0];
    name, pwd: ROPE;
    [name, pwd] ← UserCredentials.GetUserCredentials[];
    err ← IO.ROS[];
    err.PutF["Starting backup of Alpine files %g to [%g]<%g>\n",
      IO.rope[alpineFiles], IO.rope[server], IO.rope[directory]];
    MiscServerAdmin.PutLog["Starting backup of Alpine files %g to [%g]<%g>",
      IO.rope[alpineFiles], IO.rope[server], IO.rope[directory]];
    IF alpineFiles.Fetch[alpineFiles.Length[]-1] = '↑ THEN {
      alpineFiles ← alpineFiles.Substr[len: alpineFiles.Length[]-1];
      CIFS.Reset[alpineFiles ! CIFS.Error => IF code = noSuchFile THEN CONTINUE];
      in ← FileIO.Open[alpineFiles];
      }
    ELSE in ← IO.RIS[alpineFiles];
    { -- Parse the messagesTo ROPE into a LIST OF RName.
      s: STREAM = IO.RIS[messagesTo];
      to: RName;
      DO
        to ← s.GetToken[IO.IDProc];
        IF to.IsEmpty[] THEN EXIT;
        toList ← CONS[first: to, rest: toList];
        ENDLOOP;
      s.Close[];
      };
    DO
      alpineFile: ROPE;
      info: BackupFileInfo;
      alpineFile ← in.GetToken[IO.IDProc];
      IF alpineFile.IsEmpty[] THEN EXIT;
      info ← BackupFile[alpineFile, server, directory, err];
      IF info = alpineError THEN {
        -- retry once
        err.PutRope["Retrying ...\n"];
        info ← BackupFile[alpineFile, server, directory, err];
        };
      counts[info] ← counts[info] + 1;
      ENDLOOP;
    { body: ROPE;
    body ← IO.PutFR["Summary:\n%g file(s) copied\n%g file(s) found unchanged\n%g file(s) not found\n%g error(s) in IFS operations\n%g error(s) in Alpine operations\n\nLog:\n\n",
        IO.int[counts[fileCopied]], IO.int[counts[fileUnchanged]], IO.int[counts[fileNotFound]],
        IO.int[counts[ifsError]], IO.int[counts[alpineError]]];
    body ← body.Concat[IO.GetOutputStreamRope[err]];
    [] ← SendMessage[
      from: "Alpine backup process",
      sender: name, senderPwd: pwd,
      returnTo: "MBrown.pa",
      to: toList, cc: NIL,
      subject:
        IF (counts[ifsError] + counts[alpineError]) # 0 THEN
          "Ending backup (file server errors)"
        ELSE IF counts[fileNotFound] # 0 THEN
          "Ending backup (files not found)"
        ELSE "Ending backup",
      otherHeader: NIL,
      body: body,
      validate: FALSE, sendIfValidateFails: TRUE];
    };
    err.Close[];
    in.Close[];
    MiscServerAdmin.PutLog[format: "Ending backup of Alpine files"];
    EndBackup[];
    };

  BackupFileInfo: TYPE = {
    fileNotFound, ifsError, alpineError, fileUnchanged, fileCopied };

  BackupFile: PROC [alpineFile: ROPE, server, directory: ROPE, err: STREAM]
    RETURNS [BackupFileInfo] = {
    ifsServer: ROPE = Rope.Cat["[", server, "]"];
    ifsDirectory: ROPE = Rope.Cat["<", directory, ">"];
    fullName, longName, shortName: ROPE;
    result: FileLookup.Result;
    found: BOOL;
    alpineCreateTime, ifsCreateTime: System.GreenwichMeanTime;
    copyOnlyIfCreateTimeChanged: BOOL ← TRUE;
    IF alpineFile.Fetch[0] = '= THEN {
      copyOnlyIfCreateTimeChanged ← FALSE;
      alpineFile ← alpineFile.Substr[1];
      };
    [longName, shortName] ← BackupFileName[alpineFile, ifsDirectory];
    fullName ← Rope.Cat[ifsServer, longName];
    { ENABLE ANY => GOTO alpineError;
    [found, alpineCreateTime] ← AlpineCmds.WheelGetCreateTime[
      file: alpineFile, enableWheel: TRUE];
    EXITS alpineError => {
      err.PutF["Alpine error in examining %g\n", IO.rope[alpineFile]];
      MiscServerAdmin.PutLog["Alpine error in examining %g", IO.rope[alpineFile]];
      RETURN [alpineError];
      }
    };
    IF NOT found THEN {
      err.PutF["Alpine file %g not found\n", IO.rope[alpineFile]];
      MiscServerAdmin.PutLog["Alpine file %g not found", IO.rope[alpineFile]];
      RETURN[fileNotFound];
      };
    [result: result, create: ifsCreateTime] ← FileLookup.LookupFile[
      server: server, file: Rope.Cat[longName, "!h"]];
    IF result = noResponse OR result = noSuchPort THEN {
      err.PutF["IFS error in examining %g on %g\n",
        IO.rope[longName], IO.rope[server]];
      MiscServerAdmin.PutLog["IFS error in examining %g on %g",
        IO.rope[longName], IO.rope[server]];
      RETURN[ifsError];
      };
    IF result = ok AND
      copyOnlyIfCreateTimeChanged AND alpineCreateTime = ifsCreateTime THEN {
      err.PutF["File found unchanged %g\n", IO.rope[alpineFile]];
      MiscServerAdmin.PutLog["File found unchanged %g", IO.rope[alpineFile]];
      RETURN[fileUnchanged];
      };
    { ENABLE ANY => GOTO alpineError;
    AlpineCmds.WheelCopy[from: alpineFile, to: "Backup.bigFile$$", enableWheel: TRUE];
    EXITS alpineError => {
      err.PutF["Alpine error in retrieving %g\n", IO.rope[alpineFile]];
      MiscServerAdmin.PutLog["Alpine error in retrieving %g", IO.rope[alpineFile]];
      RETURN [alpineError];
      }
    };
    TRUSTED { ENABLE ANY => GOTO ifsError;
    source: CIFS.OpenFile = CIFS.Open["Backup.bigFile$$", CIFS.read];
    [] ← FtpMan.Store[server, longName, CIFS.GetFC[source]];
    source.Close[];
    EXITS ifsError => {
      err.PutF["IFS error in storing %g on %g\n",
        IO.rope[longName], IO.rope[server]];
      MiscServerAdmin.PutLog["IFS error in storing %g on %g",
        IO.rope[longName], IO.rope[server]];
      RETURN [ifsError];
      }
    };
    DelVerAndSetNoBackup[server, directory, longName];
    err.PutF["File transferred okay %g\n", IO.rope[alpineFile]];
    MiscServerAdmin.PutLog["File transferred okay %g", IO.rope[alpineFile]];
    RETURN [fileCopied]
    };

  BackupFileName: PROC [alpineFile, ifsDirectory: ROPE]
    RETURNS [longName, shortName: ROPE] = {
    -- Transforms "[Luther.alpine]<MBrown.pa>Walnut.DBLog", "<AlpineBackup>"
    -- into "<AlpineBackup>Luther>MBrown.pa>Walnut.DBLog", "Walnut.DBLog".
    IF NOT Rope.Match[pattern: "[*.alpine]<*>*", object: alpineFile, case: FALSE] THEN
      ERROR;
    { -- derive longName from alpineFile and ifsDirectory
      longName ← alpineFile.Replace[
        start: alpineFile.Find[s2: ".alpine]<", case: FALSE], len: 9, with: ">"];
      longName ← longName.Replace[start: 0, len: 1, with: ifsDirectory];
      };
    { -- derive shortName from longName
      pos1: INT ← 0;
      pos2: INT;
      UNTIL (pos2 ← longName.Find[s2: ">", pos1: pos1+1]) = -1 DO
        pos1 ← pos2;
        ENDLOOP;
      shortName ← longName.Substr[start: pos1+1];
      };
    RETURN [longName, shortName];
    };

  DelVerAndSetNoBackup: PROC [server, directory, longName: ROPE] = {
    -- server is like "Ivy"
    -- directory is like "AlpineBackup"
    -- longName is like "<AlpineBackup>Luther>MBrown.pa>Walnut.DBLog"
    ksFile: STREAM = FileIO.Open["Backup.ks$$", overwrite];
    ksFile.PutRope[Rope.Cat[
      "Connect ", directory, "\n",
      "Delete ", longName, ",\nKeep 1\nConfirm\n\n"]];
    ksFile.PutRope[Rope.Cat[
      "Change Attributes ", longName, "\nNo Backup\n\nQuit\n"]];
    ksFile.Close[];
    UserExec.DoIt[Rope.Cat["Chat ", server, " -l >Backup.ChatLog$$ <Backup.ks$$ -d -s"]];
    };

  RName: TYPE = GVBasics.RName;

  SendMessageInfo: TYPE = {
    ok, invalidRecipient, noValidRecipients, badPwd, badSender, badReturnTo, allDown};

  SendMessage: PROC [from: ROPE, sender: RName, senderPwd: ROPE, returnTo: RName,
    to, cc: LIST OF RName, subject: ROPE, otherHeader: ROPE, body: ROPE,
    validate, sendIfValidateFails: BOOL]
    RETURNS [sent: BOOL, info: SendMessageInfo] = {
    NoticeInvalidRecipient: PROC [rNumber: INT, rName: RName] = {
      state ← $invalidRecipient;
      };
    h: GVSend.Handle = GVSend.Create[];
    state: SendMessageInfo;
    aborted: BOOL ← FALSE;
    DO
      ENABLE GVSend.SendFailed => IF notDelivered THEN LOOP ELSE EXIT;
      SELECT h.StartSend[senderPwd, sender, returnTo, validate] FROM
        ok => state ← $ok;
        badPwd => { state ← $badPwd;  EXIT };
        badSender => { state ← $badSender;  EXIT };
        badReturnTo => { state ← $badReturnTo;  EXIT };
        allDown => { state ← $allDown; EXIT };
        ENDCASE => ERROR;
      AddRecipients[h, to];
      AddRecipients[h, cc];
      IF validate THEN {
        IF h.CheckValidity[NoticeInvalidRecipient] = 0 THEN state ← $noValidRecipients;
        IF NOT sendIfValidateFails AND state # $ok THEN {
          h.Abort[];  aborted ← TRUE;  EXIT;
          };
        };
      h.StartItem[Text];
      h.AddToItem[ConsMessage[sender, subject, from, to, cc, otherHeader, body]];
      h.Send[];
      EXIT;
      ENDLOOP;
    h.Close[];
    RETURN [state IN [$ok .. $noValidRecipients] AND NOT aborted, state]
    };

  AddRecipients: PROC [h: GVSend.Handle, recipients: LIST OF RName] = {
    FOR r: LIST OF RName ← recipients, r.rest UNTIL r = NIL DO
      h.AddRecipient[recipient: r.first];
      ENDLOOP;
    };

  ConsMessage: PROC [
    sender: RName, subject, from: ROPE, to, cc: LIST OF RName, otherHeader, body: ROPE]
    RETURNS [ROPE] = {
    dateRope: ROPE = ConsField["Date", IO.PutFR["%g", IO.time[]]];
    senderRope: ROPE = ConsField["Sender", sender];
    subjectRope: ROPE = ConsField["Subject", subject];
    fromRope: ROPE = ConsField["From", from];
    toRope: ROPE = ConsListField["To", to];
    ccRope: ROPE = ConsListField["cc", cc];
    header: ROPE = Rope.Cat[dateRope, senderRope, subjectRope, fromRope, toRope, ccRope];
    RETURN[Rope.Cat[header, otherHeader, "\n", body]];
    };

  ConsField: PROC [fieldName, fieldContents: ROPE] RETURNS [ROPE] = {
    RETURN [IF fieldContents.IsEmpty[] THEN NIL
      ELSE IO.PutFR["%g: %g\n", IO.rope[fieldName], IO.rope[fieldContents]]];
    };

  ConsListField: PROC [fieldName: ROPE, fieldContents: LIST OF ROPE] RETURNS [ROPE]= {
    result: ROPE ← NIL;
    IF fieldContents # NIL THEN {
      result ← fieldName.Concat[": "];
      DO
        result ← result.Concat[fieldContents.first];
        fieldContents ← fieldContents.rest;
        IF fieldContents = NIL THEN EXIT;
        result ← result.Concat[", "];
        ENDLOOP;
      result ← result.Concat["\n"];
      };
    RETURN [result.Flatten[]];
    };

  Init: PROC [] = TRUSTED {
    Process.Detach[FORK BackupProcess[when: 5]];
    MiscServerAdmin.AddCommand[command: 
      [name: $StartAlpineBackup, arg: LIST["alpineFiles: ", "messagesTo: "]],
      details: [data: NIL, proc: StartAlpineBackup, acl: "AlpineWheels↑.pa"]];
    };

  Init[];

END.


CHANGE LOG

Created by MBrown on May 28, 1983 9:20 pm

Changed by MBrown on July 8, 1983 11:53 am
-- Added code for server operation (periodic wakeup, grapevine message sending.)

Changed by MBrown on July 19, 1983 8:35 am
-- Added code for triggering backup from MiscServerTool

Changed by MBrown on July 22, 1983 4:30 pm
-- Added code for passing "alpineFiles" as a value as well as a file reference; added
--"messagesTo" button to aid in debugging.