home *** CD-ROM | disk | FTP | other *** search
/ Usenet 1994 October / usenetsourcesnewsgroupsinfomagicoctober1994disk2.iso / unix / volume2 / rtar < prev    next >
Internet Message Format  |  1986-11-30  |  16KB

  1. From: Arnold Robbins <linus!gatech!arnold>
  2. Subject: Diffs to tar to use a remote system's tape drive
  3. Newsgroups: mod.sources
  4. Approved: john@genrad.UUCP
  5.  
  6. Mod.sources:  Volume 2, Issue 5
  7. Submitted by: Arnold Robbins <linus!gatech!arnold>
  8.  
  9.  
  10. * WARNING:  The man page rmt(8) for /etc/rmt documents the remote mag
  11. * tape protocol which rdump and rrestore use.  Unfortunately, the man
  12. * page is *WRONG*.  The author of the routines I'm including originally
  13. * wrote his code just based on the man page, and it didn't work, so he
  14. * went to the rdump source to figure out why.  The only thing he had to
  15. * change was to check for the 'F' return code in addition to the 'E',
  16. * and to separate the various arguments with \n instead of a space.  I
  17. * personally don't think that this is much of a problem, but I wanted to
  18. * point it out.
  19. *
  20. *   Arnold Robbins
  21.  
  22.  
  23. The following context diffs to the 4.2 BSD tar.c and man page allow tar
  24. to read and write tapes on a remote system's tape drive.  The routines
  25. at the end that deal with /etc/rmt across a pipe are general purpose and
  26. in the public domain.
  27.  
  28. It is interesting to note that what rmt(8) says about its protocol, and what
  29. you actually have to do to get it to work are quite different.
  30.  
  31. The command line syntax has not changed.  Instead, the file name for the -f
  32. option looks sorta like what rcp takes, e.g.
  33.  
  34.     $ tar -cvf gatech:/dev/rmt8 /usr/src
  35.  
  36. In particular, if "/dev/" does not follow the colon, tar decides it is using
  37. a regular, local file.
  38.  
  39. The diffs are for the tar that comes with BRL Unix. Your line numbers
  40. may vary.
  41.  
  42. Arnold Robbins        (rmt stuff courtesy of Jeff Lee, gatech!jeff)
  43. arnold@gatech.{CSNET, UUCP}
  44. --------------------- cut here ----------------------------
  45. *** /usr/src/bin/tar.c    Wed Nov 14 00:09:23 1984
  46. --- tar.c    Mon Jul  1 14:25:40 1985
  47. ***************
  48. *** 115,120
  49.   char    *getcwd();
  50.   char    *getwd();
  51.   
  52.   main(argc, argv)
  53.   int    argc;
  54.   char    *argv[];
  55.  
  56. --- 115,141 -----
  57.   char    *getcwd();
  58.   char    *getwd();
  59.   
  60. + int    open();
  61. + int    read();
  62. + int    write();
  63. + int    close();
  64. + int    ioctl();
  65. + long    lseek();
  66. + int    rmtopen();
  67. + int    rmtread();
  68. + int    rmtwrite();
  69. + int    rmtclose();
  70. + int    rmtioctl();
  71. + long    rmtlseek();
  72. + int    (*t_open)();
  73. + int    (*t_read)();
  74. + int    (*t_write)();
  75. + int    (*t_close)();
  76. + int    (*t_ioctl)();
  77. + long    (*t_lseek)();
  78.   main(argc, argv)
  79.   int    argc;
  80.   char    *argv[];
  81. ***************
  82. *** 264,269
  83.               nblock);
  84.           done(1);
  85.       }
  86.       if (rflag) {
  87.           if (cflag && tfile != NULL)
  88.               usage();
  89.  
  90. --- 285,293 -----
  91.               nblock);
  92.           done(1);
  93.       }
  94. +     fix_remote();
  95.       if (rflag) {
  96.           if (cflag && tfile != NULL)
  97.               usage();
  98. ***************
  99. *** 285,291
  100.               }
  101.               mt = dup(1);
  102.               nblock = 1;
  103. !         } else if ((mt = open(usefile, 2)) < 0) {
  104.               if (cflag == 0 || (mt =  creat(usefile, 0666)) < 0) {
  105.                   fprintf(stderr,
  106.                       "tar: cannot open %s\n", usefile);
  107.  
  108. --- 309,315 -----
  109.               }
  110.               mt = dup(1);
  111.               nblock = 1;
  112. !         } else if ((mt = (*t_open)(usefile, 2)) < 0) {
  113.               if (cflag == 0 || (mt =  creat(usefile, 0666)) < 0) {
  114.                   fprintf(stderr,
  115.                       "tar: cannot open %s\n", usefile);
  116. ***************
  117. *** 298,304
  118.       if (strcmp(usefile, "-") == 0) {
  119.           mt = dup(0);
  120.           nblock = 1;
  121. !     } else if ((mt = open(usefile, 0)) < 0) {
  122.           fprintf(stderr, "tar: cannot open %s\n", usefile);
  123.           done(1);
  124.       }
  125.  
  126. --- 322,328 -----
  127.       if (strcmp(usefile, "-") == 0) {
  128.           mt = dup(0);
  129.           nblock = 1;
  130. !     } else if ((mt = (*t_open)(usefile, 0)) < 0) {
  131.           fprintf(stderr, "tar: cannot open %s\n", usefile);
  132.           done(1);
  133.       }
  134. ***************
  135. *** 306,311
  136.           doxtract(argv);
  137.       else
  138.           dotable();
  139.       done(0);
  140.   }
  141.   
  142.  
  143. --- 330,336 -----
  144.           doxtract(argv);
  145.       else
  146.           dotable();
  147. +     (*t_close) (mt);
  148.       done(0);
  149.   }
  150.   
  151. ***************
  152. *** 1195,1201
  153.   {
  154.       first = 1;
  155.       if (recno >= nblock) {
  156. !         if (write(mt, tbuf, TBLOCK*nblock) < 0) {
  157.               fprintf(stderr, "tar: tape write error\n");
  158.               done(2);
  159.           }
  160.  
  161. --- 1220,1226 -----
  162.   {
  163.       first = 1;
  164.       if (recno >= nblock) {
  165. !         if ((*t_write)(mt, tbuf, TBLOCK*nblock) < 0) {
  166.               fprintf(stderr, "tar: tape write error\n");
  167.               done(2);
  168.           }
  169. ***************
  170. *** 1209,1215
  171.        *  residual to the tape buffer.
  172.        */
  173.       while (recno == 0 && n >= nblock) {
  174. !         if (write(mt, buffer, TBLOCK*nblock) < 0) {
  175.               fprintf(stderr, "tar: tape write error\n");
  176.               done(2);
  177.           }
  178.  
  179. --- 1234,1240 -----
  180.        *  residual to the tape buffer.
  181.        */
  182.       while (recno == 0 && n >= nblock) {
  183. !         if ((*t_write)(mt, buffer, TBLOCK*nblock) < 0) {
  184.               fprintf(stderr, "tar: tape write error\n");
  185.               done(2);
  186.           }
  187. ***************
  188. *** 1221,1227
  189.           bcopy(buffer, (char *)&tbuf[recno++], TBLOCK);
  190.           buffer += TBLOCK;
  191.           if (recno >= nblock) {
  192. !             if (write(mt, tbuf, TBLOCK*nblock) < 0) {
  193.                   fprintf(stderr, "tar: tape write error\n");
  194.                   done(2);
  195.               }
  196.  
  197. --- 1246,1252 -----
  198.           bcopy(buffer, (char *)&tbuf[recno++], TBLOCK);
  199.           buffer += TBLOCK;
  200.           if (recno >= nblock) {
  201. !             if ((*t_write)(mt, tbuf, TBLOCK*nblock) < 0) {
  202.                   fprintf(stderr, "tar: tape write error\n");
  203.                   done(2);
  204.               }
  205. ***************
  206. *** 1240,1246
  207.       struct mtget mtget;
  208.   
  209.       if (mtdev == 1)
  210. !         mtdev = ioctl(mt, MTIOCGET, &mtget);
  211.       if (mtdev == 0) {
  212.           if (ioctl(mt, MTIOCTOP, &mtop) < 0) {
  213.               fprintf(stderr, "tar: tape backspace error\n");
  214.  
  215. --- 1265,1271 -----
  216.       struct mtget mtget;
  217.   
  218.       if (mtdev == 1)
  219. !         mtdev = (*t_ioctl)(mt, MTIOCGET, &mtget);
  220.       if (mtdev == 0) {
  221.           if ((*t_ioctl)(mt, MTIOCTOP, &mtop) < 0) {
  222.               fprintf(stderr, "tar: tape backspace error\n");
  223. ***************
  224. *** 1242,1248
  225.       if (mtdev == 1)
  226.           mtdev = ioctl(mt, MTIOCGET, &mtget);
  227.       if (mtdev == 0) {
  228. !         if (ioctl(mt, MTIOCTOP, &mtop) < 0) {
  229.               fprintf(stderr, "tar: tape backspace error\n");
  230.               done(4);
  231.           }
  232.  
  233. --- 1267,1273 -----
  234.       if (mtdev == 1)
  235.           mtdev = (*t_ioctl)(mt, MTIOCGET, &mtget);
  236.       if (mtdev == 0) {
  237. !         if ((*t_ioctl)(mt, MTIOCTOP, &mtop) < 0) {
  238.               fprintf(stderr, "tar: tape backspace error\n");
  239.               done(4);
  240.           }
  241. ***************
  242. *** 1247,1253
  243.               done(4);
  244.           }
  245.       } else
  246. !         lseek(mt, (long) -TBLOCK*nblock, 1);
  247.       recno--;
  248.   }
  249.   
  250.  
  251. --- 1272,1278 -----
  252.               done(4);
  253.           }
  254.       } else
  255. !         (*t_lseek)(mt, (long) -TBLOCK*nblock, 1);
  256.       recno--;
  257.   }
  258.   
  259. ***************
  260. *** 1253,1259
  261.   
  262.   flushtape()
  263.   {
  264. !     write(mt, tbuf, TBLOCK*nblock);
  265.   }
  266.   
  267.   bread(fd, buf, size)
  268.  
  269. --- 1278,1284 -----
  270.   
  271.   flushtape()
  272.   {
  273. !     (*t_write)(mt, tbuf, TBLOCK*nblock);
  274.   }
  275.   
  276.   bread(fd, buf, size)
  277. ***************
  278. *** 1265,1271
  279.       static int lastread = 0;
  280.   
  281.       if (!Bflag)
  282. !         return (read(fd, buf, size));
  283.       for (count = 0; count < size; count += lastread) {
  284.           if (lastread < 0) {
  285.               if (count > 0)
  286.  
  287. --- 1290,1296 -----
  288.       static int lastread = 0;
  289.   
  290.       if (!Bflag)
  291. !         return ((*t_read)(fd, buf, size));
  292.       for (count = 0; count < size; count += lastread) {
  293.           if (lastread < 0) {
  294.               if (count > 0)
  295. ***************
  296. *** 1272,1278
  297.                   return (count);
  298.               return (lastread);
  299.           }
  300. !         lastread = read(fd, buf, size - count);
  301.           buf += lastread;
  302.       }
  303.       return (count);
  304.  
  305. --- 1297,1303 -----
  306.                   return (count);
  307.               return (lastread);
  308.           }
  309. !         lastread = (*t_read)(fd, buf, size - count);
  310.           buf += lastread;
  311.       }
  312.       return (count);
  313. ***************
  314. *** 1288,1291
  315.           exit(1);
  316.       }
  317.       return (buf);
  318.   }
  319.  
  320. --- 1313,1758 -----
  321.           exit(1);
  322.       }
  323.       return (buf);
  324. + }
  325. + fix_remote ()
  326. + {
  327. +     char *cp, *index ();
  328. +     if ((cp = index (usefile, ':')) == NULL ||
  329. +         strncmp (cp + 1, "/dev/", 5) != 0)
  330. +     {
  331. +         t_open = open;
  332. +         t_read = read;
  333. +         t_write = write;
  334. +         t_close = close;
  335. +         t_ioctl = ioctl;
  336. +         t_lseek = lseek;
  337. +     }
  338. +     else
  339. +     {
  340. +         t_open = rmtopen;
  341. +         t_read = rmtread;
  342. +         t_write = rmtwrite;
  343. +         t_close = rmtclose;
  344. +         t_ioctl = rmtioctl;
  345. +         t_lseek = rmtlseek;
  346. +     }
  347. + }
  348. + /*
  349. + *    rmt --- remote tape emulator subroutines
  350. + *
  351. + *    Originally written by Jeff Lee, modified some by Arnold Robbins
  352. + */
  353. + /* these are included above, except setjmp.h */
  354. + /*
  355. + #include <stdio.h>
  356. + #include <errno.h>
  357. + #include <setjmp.h>
  358. + #include <signal.h>
  359. + #include <sys/types.h>
  360. + #include <sys/ioctl.h>
  361. + #include <sys/mtio.h>
  362. + */
  363. + #include <setjmp.h>
  364. + /*
  365. + *    MAXUNIT --- Maximum number of remote tape file units
  366. + *
  367. + *    READ --- Return the number of the read side file descriptor
  368. + *    WRITE --- Return the number of the write side file descriptor
  369. + */
  370. + #define MAXUNIT    4
  371. + #define READ(fd)    (Ctp[fd][0])
  372. + #define WRITE(fd)    (Ptc[fd][1])
  373. + static int Ctp[MAXUNIT][2] = { -1, -1, -1, -1, -1, -1, -1, -1 };
  374. + static int Ptc[MAXUNIT][2] = { -1, -1, -1, -1, -1, -1, -1, -1 };
  375. + jmp_buf Jmpbuf;
  376. + extern int errno;
  377. + /*
  378. + *    abort --- close off a remote tape connection
  379. + */
  380. + static abort(tfd)
  381. + int tfd;
  382. + {
  383. +     close(READ(tfd));
  384. +     close(WRITE(tfd));
  385. +     READ(tfd) = -1;
  386. +     WRITE(tfd) = -1;
  387. + }
  388. + /*
  389. + *    command --- attempt to perform a remote tape command
  390. + */
  391. + static command(tfd, buf)
  392. + char *buf;
  393. + int tfd;
  394. + {
  395. +     int blen;
  396. +     int (*pstat)();
  397. + /*
  398. + *    save current pipe status and try to make the request
  399. + */
  400. +     blen = strlen(buf);
  401. +     pstat = signal(SIGPIPE, SIG_IGN);
  402. +     if (write(WRITE(tfd), buf, blen) == blen)
  403. +     {
  404. +         signal(SIGPIPE, pstat);
  405. +         return(0);
  406. +     }
  407. + /*
  408. + *    something went wrong. close down and go home
  409. + */
  410. +     signal(SIGPIPE, pstat);
  411. +     abort(tfd);
  412. +     errno = EIO;
  413. +     return(-1);
  414. + }
  415. + /*
  416. + *    status --- retrieve the status from the pipe
  417. + */
  418. + static status(tfd)
  419. + int tfd;
  420. + {
  421. +     int i;
  422. +     char c, *cp;
  423. +     char buf[64];
  424. + /*
  425. + *    read the reply command line
  426. + */
  427. +     for (i = 0, cp = buf; i < 64; i++, cp++)
  428. +     {
  429. +         if (read(READ(tfd), cp, 1) != 1)
  430. +         {
  431. +             abort(tfd);
  432. +             errno = EIO;
  433. +             return(-1);
  434. +         }
  435. +         if (*cp == '\n')
  436. +         {
  437. +             *cp = 0;
  438. +             break;
  439. +         }
  440. +     }
  441. +     if (i == 64)
  442. +     {
  443. +         abort(tfd);
  444. +         errno = EIO;
  445. +         return(-1);
  446. +     }
  447. + /*
  448. + *    check the return status
  449. + */
  450. +     for (cp = buf; *cp; cp++)
  451. +         if (*cp != ' ')
  452. +             break;
  453. +     if (*cp == 'E' || *cp == 'F')
  454. +     {
  455. +         errno = atoi(cp + 1);
  456. +         while (read(READ(tfd), &c, 1) == 1)
  457. +             if (c == '\n')
  458. +                 break;
  459. +         if (*cp == 'F')
  460. +             abort(tfd);
  461. +         return(-1);
  462. +     }
  463. + /*
  464. + *    check for mis-synced pipes
  465. + */
  466. +     if (*cp != 'A')
  467. +     {
  468. +         abort(tfd);
  469. +         errno = EIO;
  470. +         return(-1);
  471. +     }
  472. +     return(atoi(cp + 1));
  473. + }
  474. + /*
  475. + *    rmtopen --- open a magtape device on system specified
  476. + */
  477. + rmtopen(dev, mode)
  478. + char *dev;
  479. + int mode;
  480. + {
  481. +     int i, rc;
  482. +     char buf[64];
  483. +     char *sys;
  484. + /*
  485. + *    first, find an open pair of file descriptors
  486. + */
  487. +     for (i = 0; i < MAXUNIT; i++)
  488. +         if (READ(i) == -1 && WRITE(i) == -1)
  489. +             break;
  490. +     if (i == MAXUNIT)
  491. +     {
  492. +         errno = EMFILE;
  493. +         return(-1);
  494. +     }
  495. + /*
  496. + *    pull apart system and device
  497. + */
  498. +     for (sys = dev; *dev != ':'; dev++)
  499. +         ;
  500. +     *dev++ = '\0';
  501. + /*
  502. + *    setup the pipes for the 'rsh' command and fork
  503. + */
  504. +     if (pipe(Ptc[i]) == -1 || pipe(Ctp[i]) == -1)
  505. +         return(-1);
  506. +     if ((rc = fork()) == -1)
  507. +         return(-1);
  508. +     if (rc == 0)
  509. +     {
  510. +         close(0);
  511. +         dup(Ptc[i][0]);
  512. +         close(Ptc[i][0]); close(Ptc[i][1]);
  513. +         close(1);
  514. +         dup(Ctp[i][1]);
  515. +         close(Ctp[i][0]); close(Ctp[i][1]);
  516. +         execl("/usr/ucb/rsh", "rsh", sys, "/etc/rmt", (char *) 0);
  517. + /*
  518. + *    bad problems if we get here
  519. + */
  520. +         perror("exec");
  521. +         exit(1);
  522. +     }
  523. +     close(Ptc[i][0]); close(Ctp[i][1]);
  524. + /*
  525. + *    now attempt to open the tape device
  526. + */
  527. +     sprintf(buf, "O%s\n%d\n", dev, mode);
  528. +     if (command(i, buf) == -1 || status(i) == -1)
  529. +         return(-1);
  530. +     return(i);
  531. + }
  532. + /*
  533. + *    rmtclose --- close a remote magtape unit and shut down
  534. + */
  535. + rmtclose(tfd)
  536. + int tfd;
  537. + {
  538. +     int rc;
  539. +     if (command(tfd, "C\n") != -1)
  540. +     {
  541. +         rc = status(tfd);
  542. +         abort(tfd);
  543. +         return(rc);
  544. +     }
  545. +     return(-1);
  546. + }
  547. + /*
  548. + *    rmtread --- read a buffer from a remote tape
  549. + */
  550. + rmtread(tfd, data, cnt)
  551. + int tfd, cnt;
  552. + char *data;
  553. + {
  554. +     int rc, i;
  555. +     char buf[64];
  556. +     sprintf(buf, "R%d\n", cnt);
  557. +     if (command(tfd, buf) == -1 || (rc = status(tfd)) == -1)
  558. +         return(-1);
  559. +     for (i = 0; i < rc; i += cnt, data += cnt)
  560. +     {
  561. +         cnt = read(READ(tfd), data, rc);
  562. +         if (cnt <= 0)
  563. +         {
  564. +             abort(tfd);
  565. +             errno = EIO;
  566. +             return(-1);
  567. +         }
  568. +     }
  569. +     return(rc);
  570. + }
  571. + /*
  572. + *    rmtwrite --- write a buffer to the remote tape
  573. + */
  574. + rmtwrite(tfd, data, cnt)
  575. + int tfd, cnt;
  576. + char *data;
  577. + {
  578. +     int rc;
  579. +     char buf[64];
  580. +     int (*pstat)();
  581. +     sprintf(buf, "W%d\n", cnt);
  582. +     if (command(tfd, buf) == -1)
  583. +         return(-1);
  584. +     pstat = signal(SIGPIPE, SIG_IGN);
  585. +     if (write(WRITE(tfd), data, cnt) == cnt)
  586. +         return(status(tfd));
  587. +     abort(tfd);
  588. +     errno = EIO;
  589. +     return(-1);
  590. + }
  591. + /*
  592. + *    rmtlseek --- perform an imitation lseek operation remotely
  593. + */
  594. + rmtlseek(tfd, wh, off)
  595. + int tfd, wh, off;
  596. + {
  597. +     char buf[64];
  598. +     sprintf(buf, "L%d\n%d\n", wh, off);
  599. +     if (command(tfd, buf) == -1)
  600. +         return(-1);
  601. +     return(status(tfd));
  602. + }
  603. + /*
  604. + *    rmtioctl --- perform raw tape operations remotely
  605. + */
  606. + rmtioctl(tfd, op, arg)
  607. + int tfd, op;
  608. + char *arg;
  609. + {
  610. +     char c;
  611. +     int rc, cnt;
  612. +     char buf[64];
  613. + /*
  614. + *    MTIOCOP is the easy one. nothing is transfered in binary
  615. + */
  616. +     if (op == MTIOCTOP)
  617. +     {
  618. +         sprintf(buf, "I%d\n%d\n", ((struct mtop *) arg)->mt_op,
  619. +             ((struct mtop *) arg)->mt_count);
  620. +         if (command(tfd, buf) == -1)
  621. +             return(-1);
  622. +         return(status(tfd));
  623. +     }
  624. + /*
  625. + *    we can only handle 2 ops, if not the other one, punt
  626. + */
  627. +     if (op != MTIOCGET)
  628. +     {
  629. +         errno = EINVAL;
  630. +         return(-1);
  631. +     }
  632. + /*
  633. + *    grab the status and read it directly into the structure
  634. + *    this assumes that the status buffer is (hopefully) not
  635. + *    padded and that 2 shorts fit in a long without any word
  636. + *    alignment problems, ie - the whole struct is contiguous
  637. + *    NOTE - this is probably NOT a good assumption.
  638. + */
  639. +     if (command(tfd, "S\n") == -1 || (rc = status(tfd)) == -1)
  640. +         return(-1);
  641. +     for (; rc > 0; rc -= cnt, arg += cnt)
  642. +     {
  643. +         cnt = read(READ(tfd), arg, rc);
  644. +         if (cnt <= 0)
  645. +         {
  646. +             abort(tfd);
  647. +             errno = EIO;
  648. +             return(-1);
  649. +         }
  650. +     }
  651. + /*
  652. + *    now we check for byte position. mt_type is a small integer field
  653. + *    (normally) so we will check its magnitude. if it is larger than
  654. + *    256, we will assume that the bytes are swapped and go through
  655. + *    and reverse all the bytes
  656. + */
  657. +     if (((struct mtget *) arg)->mt_type < 256)
  658. +         return(0);
  659. +     for (cnt = 0; cnt < rc; cnt += 2)
  660. +     {
  661. +         c = arg[cnt];
  662. +         arg[cnt] = arg[cnt+1];
  663. +         arg[cnt+1] = c;
  664. +     }
  665. +     return(0);
  666.   }
  667. *** /usr/man/man1/tar.1    Mon Jun 27 00:35:14 1983
  668. --- tar.1    Mon Jul  1 13:51:09 1985
  669. ***************
  670. *** 95,101
  671.   .B f
  672.   .I Tar
  673.   uses the next argument as the name of the archive instead of
  674. ! /dev/rmt?. If the name of the file is `\-', tar writes to standard output or
  675.   reads from standard input, whichever is appropriate. Thus,
  676.   .I tar
  677.   can be used as the head or tail of a filter chain.
  678.  
  679. --- 95,113 -----
  680.   .B f
  681.   .I Tar
  682.   uses the next argument as the name of the archive instead of
  683. ! /dev/rmt?.
  684. ! .sp
  685. ! If the file name has the form
  686. ! .IR system :/dev/???,
  687. ! .I tar
  688. ! will use the tape drive /dev/??? on the remote system
  689. ! .IR system ,
  690. ! via
  691. ! .IR rsh (1),
  692. ! and
  693. ! .IR rmt (8).
  694. ! .sp
  695. ! If the name of the file is `\-', tar writes to standard output or
  696.   reads from standard input, whichever is appropriate. Thus,
  697.   .I tar
  698.   can be used as the head or tail of a filter chain.
  699. ***************
  700. *** 179,181
  701.   The current limit on file name length is 100 characters.
  702.   .br
  703.   There is no way to selectively follow symbolic links.
  704.  
  705. --- 191,195 -----
  706.   The current limit on file name length is 100 characters.
  707.   .br
  708.   There is no way to selectively follow symbolic links.
  709. + .br
  710. + Using a remote system's tape drive can be slow.
  711.  
  712.