2 * The new sysinstall program.
4 * This is probably the last attempt in the `sysinstall' line, the next
5 * generation being slated to essentially a complete rewrite.
10 * Jordan Hubbard. All rights reserved.
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
15 * 1. Redistributions of source code must retain the above copyright
16 * notice, this list of conditions and the following disclaimer,
17 * verbatim and that no modifications are made prior to this
19 * 2. Redistributions in binary form must reproduce the above copyright
20 * notice, this list of conditions and the following disclaimer in the
21 * documentation and/or other materials provided with the distribution.
23 * THIS SOFTWARE IS PROVIDED BY JORDAN HUBBARD ``AS IS'' AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED. IN NO EVENT SHALL JORDAN HUBBARD OR HIS PETS BE LIABLE
27 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29 * OR SERVICES; LOSS OF USE, DATA, LIFE OR PROFITS; OR BUSINESS INTERRUPTION)
30 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
37 #include "sysinstall.h"
40 #include <sys/socket.h>
41 #include <sys/param.h>
42 #include <sys/mount.h>
43 #include <sys/errno.h>
44 #include <sys/fcntl.h>
49 #include <netinet/in.h>
50 #include <arpa/inet.h>
53 static Boolean got_intr = FALSE;
54 static Boolean ftp_skip_resolve = FALSE;
60 msgDebug("User generated interrupt.\n");
65 check_for_interrupt(void)
75 genericHook(dialogMenuItem *self, DeviceType type)
79 devs = deviceFind(self->prompt, type);
81 mediaDevice = devs[0];
82 return (devs ? DITEM_LEAVE_MENU : DITEM_FAILURE);
86 cdromHook(dialogMenuItem *self)
88 return genericHook(self, DEVICE_TYPE_CDROM);
94 static Boolean initted = FALSE;
98 cp = variable_get(VAR_MEDIA_TIMEOUT);
100 time = MEDIA_TIMEOUT;
107 _res.retry = 2; /* 2 times seems a reasonable number to me */
108 _res.retrans = time / 2; /* so spend half our alloted time on each try */
116 char *cp = variable_get(VAR_CPIO_VERBOSITY);
118 if (cp && !strcmp(cp, "high"))
120 else if (cp && !strcmp(cp, "medium"))
129 DEVICE_SHUTDOWN(mediaDevice);
134 * Return 1 if we successfully found and set the installation type to
138 mediaSetCDROM(dialogMenuItem *self)
144 devs = deviceFind(NULL, DEVICE_TYPE_CDROM);
145 cnt = deviceCount(devs);
147 if (self) /* Interactive? */
148 msgConfirm("No CD/DVD devices found! Please check that your system's\n"
149 "configuration is correct and that the CD/DVD drive is of a supported\n"
150 "type. For more information, consult the hardware guide\n"
152 return DITEM_FAILURE | DITEM_CONTINUE;
158 menu = deviceCreateMenu(&MenuMediaCDROM, DEVICE_TYPE_CDROM, cdromHook, NULL);
160 msgFatal("Unable to create CDROM menu! Something is seriously wrong.");
161 status = dmenuOpenSimple(menu, FALSE);
164 return DITEM_FAILURE;
167 mediaDevice = devs[0];
168 return (mediaDevice ? DITEM_SUCCESS | DITEM_LEAVE_MENU : DITEM_FAILURE);
172 floppyHook(dialogMenuItem *self)
174 return genericHook(self, DEVICE_TYPE_FLOPPY);
178 * Return 1 if we successfully found and set the installation type to
182 mediaSetFloppy(dialogMenuItem *self)
188 devs = deviceFind(NULL, DEVICE_TYPE_FLOPPY);
189 cnt = deviceCount(devs);
191 msgConfirm("No floppy devices found! Please check that your system's configuration\n"
192 "is correct. For more information, consult the hardware guide in the Doc\n"
194 return DITEM_FAILURE | DITEM_CONTINUE;
200 menu = deviceCreateMenu(&MenuMediaFloppy, DEVICE_TYPE_FLOPPY, floppyHook, NULL);
202 msgFatal("Unable to create Floppy menu! Something is seriously wrong.");
203 status = dmenuOpenSimple(menu, FALSE);
206 return DITEM_FAILURE;
209 mediaDevice = devs[0];
211 mediaDevice->private = NULL;
212 return (mediaDevice ? DITEM_LEAVE_MENU : DITEM_FAILURE);
216 DOSHook(dialogMenuItem *self)
218 return genericHook(self, DEVICE_TYPE_DOS);
222 * Return 1 if we successfully found and set the installation type to
223 * be a DOS partition.
226 mediaSetDOS(dialogMenuItem *self)
232 devs = deviceFind(NULL, DEVICE_TYPE_DOS);
233 cnt = deviceCount(devs);
235 msgConfirm("No DOS primary partitions found! This installation method is unavailable");
236 return DITEM_FAILURE | DITEM_CONTINUE;
242 menu = deviceCreateMenu(&MenuMediaDOS, DEVICE_TYPE_DOS, DOSHook, NULL);
244 msgFatal("Unable to create DOS menu! Something is seriously wrong.");
245 status = dmenuOpenSimple(menu, FALSE);
248 return DITEM_FAILURE;
251 mediaDevice = devs[0];
252 return (mediaDevice ? DITEM_LEAVE_MENU : DITEM_FAILURE);
256 tapeHook(dialogMenuItem *self)
258 return genericHook(self, DEVICE_TYPE_TAPE);
262 * Return 1 if we successfully found and set the installation type to
266 mediaSetTape(dialogMenuItem *self)
272 devs = deviceFind(NULL, DEVICE_TYPE_TAPE);
273 cnt = deviceCount(devs);
275 msgConfirm("No tape drive devices found! Please check that your system's configuration\n"
276 "is correct. For more information, consult the hardware guide in the Doc\n"
278 return DITEM_FAILURE | DITEM_CONTINUE;
284 menu = deviceCreateMenu(&MenuMediaTape, DEVICE_TYPE_TAPE, tapeHook, NULL);
286 msgFatal("Unable to create tape drive menu! Something is seriously wrong.");
287 status = dmenuOpenSimple(menu, FALSE);
290 return DITEM_FAILURE;
293 mediaDevice = devs[0];
297 val = msgGetInput("/var/tmp", "Please enter the name of a temporary directory containing\n"
298 "sufficient space for holding the contents of this tape (or\n"
299 "tapes). The contents of this directory will be removed\n"
300 "after installation, so be sure to specify a directory that\n"
301 "can be erased afterwards!\n");
305 mediaDevice->private = strdup(val);
307 return (mediaDevice ? DITEM_LEAVE_MENU : DITEM_FAILURE);
311 * Return 0 if we successfully found and set the installation type to
315 mediaSetFTP(dialogMenuItem *self)
317 static Device ftpDevice;
318 char *cp, hbuf[MAXHOSTNAMELEN], *hostname, *dir;
319 struct addrinfo hints, *res;
322 static Device *networkDev = NULL;
325 cp = variable_get(VAR_FTP_PATH);
326 /* If we've been through here before ... */
327 if (networkDev && cp && msgYesNo("Re-use old FTP site selection values?"))
330 if (!dmenuOpenSimple(&MenuMediaFTP, FALSE))
331 return DITEM_FAILURE;
333 cp = variable_get(VAR_FTP_PATH);
336 return DITEM_FAILURE;
337 else if (!strcmp(cp, "other")) {
338 variable_set2(VAR_FTP_PATH, "ftp://", 0);
339 cp = variable_get_value(VAR_FTP_PATH, "Please specify the URL of a FreeBSD distribution on a\n"
340 "remote ftp site. This site must accept either anonymous\n"
341 "ftp or you should have set an ftp username and password\n"
342 "in the Options screen.\n\n"
343 "A URL looks like this: ftp://<hostname>/<path>\n"
344 "Where <path> is relative to the anonymous ftp directory or the\n"
345 "home directory of the user being logged in as.", 0);
346 if (!cp || !*cp || !strcmp(cp, "ftp://")) {
347 variable_unset(VAR_FTP_PATH);
348 return DITEM_FAILURE;
351 if (urllen >= sizeof(ftpDevice.name)) {
352 msgConfirm("Length of specified URL is %d characters. Allowable maximum is %d.",
353 urllen,sizeof(ftpDevice.name)-1);
354 variable_unset(VAR_FTP_PATH);
355 return DITEM_FAILURE;
358 if (strncmp("ftp://", cp, 6)) {
359 msgConfirm("Sorry, %s is an invalid URL!", cp);
360 variable_unset(VAR_FTP_PATH);
361 return DITEM_FAILURE;
363 SAFE_STRCPY(ftpDevice.name, cp);
364 SAFE_STRCPY(hbuf, cp + 6);
367 if (!networkDev || msgYesNo("You've already done the network configuration once,\n"
368 "would you like to skip over it now?") != 0) {
370 DEVICE_SHUTDOWN(networkDev);
371 if (!(networkDev = tcpDeviceSelect())) {
372 variable_unset(VAR_FTP_PATH);
373 return DITEM_FAILURE;
376 if (!DEVICE_INIT(networkDev)) {
378 msgDebug("mediaSetFTP: Net device init failed.\n");
379 variable_unset(VAR_FTP_PATH);
380 return DITEM_FAILURE;
382 if (*hostname == '[' && (cp = index(hostname + 1, ']')) != NULL &&
383 (*++cp == '\0' || *cp == '/' || *cp == ':')) {
388 cp = index(hostname, ':');
389 if (cp != NULL && *cp == ':') {
391 FtpPort = strtol(cp, 0, 0);
395 if ((dir = index(cp ? cp : hostname, '/')) != NULL)
398 msgDebug("hostname = `%s'\n", hostname);
399 msgDebug("dir = `%s'\n", dir ? dir : "/");
400 msgDebug("port # = `%d'\n", FtpPort);
402 if (!ftp_skip_resolve && variable_get(VAR_NAMESERVER)) {
403 msgNotify("Looking up host %s.", hostname);
405 msgDebug("Starting DNS.\n");
408 msgDebug("Looking up hostname, %s, using getaddrinfo(AI_NUMERICHOST).\n", hostname);
409 af = variable_cmp(VAR_IPV6_ENABLE, "YES") ? AF_INET : AF_UNSPEC;
410 memset(&hints, 0, sizeof(hints));
411 hints.ai_family = af;
412 hints.ai_socktype = SOCK_STREAM;
413 hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST;
414 if (getaddrinfo(hostname, NULL, &hints, &res) != 0) {
416 msgDebug("Looking up hostname, %s, using getaddrinfo().\n",
418 hints.ai_flags = AI_PASSIVE;
419 if (getaddrinfo(hostname, NULL, &hints, &res) != 0) {
420 msgConfirm("Cannot resolve hostname `%s'! Are you sure that"
421 " your\nname server, gateway and network interface are"
422 " correctly configured?", hostname);
424 DEVICE_SHUTDOWN(networkDev);
426 variable_unset(VAR_FTP_PATH);
427 return DITEM_FAILURE;
432 msgDebug("Found DNS entry for %s successfully..\n", hostname);
434 variable_set2(VAR_FTP_HOST, hostname, 0);
435 variable_set2(VAR_FTP_DIR, dir ? dir : "/", 0);
436 variable_set2(VAR_FTP_PORT, itoa(FtpPort), 0);
437 ftpDevice.type = DEVICE_TYPE_FTP;
438 ftpDevice.init = mediaInitFTP;
439 ftpDevice.get = mediaGetFTP;
440 ftpDevice.shutdown = mediaShutdownFTP;
441 ftpDevice.private = networkDev;
442 mediaDevice = &ftpDevice;
443 return DITEM_SUCCESS | DITEM_LEAVE_MENU | DITEM_RESTORE;
447 mediaSetFTPActive(dialogMenuItem *self)
449 variable_set2(VAR_FTP_STATE, "active", 0);
450 return mediaSetFTP(self);
454 mediaSetFTPPassive(dialogMenuItem *self)
456 variable_set2(VAR_FTP_STATE, "passive", 0);
457 return mediaSetFTP(self);
460 int mediaSetHTTP(dialogMenuItem *self)
464 char *cp, *idx, hbuf[MAXHOSTNAMELEN], *hostname;
466 int what = DITEM_RESTORE;
469 tmp = ftp_skip_resolve;
470 ftp_skip_resolve = TRUE;
471 result = mediaSetFTP(self);
472 ftp_skip_resolve = tmp;
474 if (DITEM_STATUS(result) != DITEM_SUCCESS)
477 cp = variable_get_value(VAR_HTTP_PROXY,
478 "Please enter the address of the HTTP proxy in this format:\n"
479 " hostname:port (the ':port' is optional, default is 3128)",0);
481 return DITEM_FAILURE;
482 SAFE_STRCPY(hbuf, cp);
484 if (*hostname == '[' && (idx = index(hostname + 1, ']')) != NULL &&
485 (*++idx == '\0' || *idx == ':')) {
489 idx = index(hostname, ':');
490 if (idx == NULL || *idx != ':')
491 HttpPort = 3128; /* try this as default */
494 HttpPort = strtol(idx, 0, 0);
497 variable_set2(VAR_HTTP_HOST, hostname, 0);
498 variable_set2(VAR_HTTP_PORT, itoa(HttpPort), 0);
500 msgDebug("VAR_FTP_PATH : %s\n",variable_get(VAR_FTP_PATH));
501 msgDebug("VAR_HTTP_HOST, _PORT: %s:%s\n",variable_get(VAR_HTTP_HOST),
502 variable_get(VAR_HTTP_PORT));
505 /* mediaDevice has been set by mediaSetFTP(), overwrite partly: */
506 mediaDevice->type = DEVICE_TYPE_HTTP;
507 mediaDevice->init = mediaInitHTTP;
508 mediaDevice->get = mediaGetHTTP;
509 mediaDevice->shutdown = dummyShutdown;
510 return DITEM_SUCCESS | DITEM_LEAVE_MENU | what;
515 mediaSetUFS(dialogMenuItem *self)
517 static Device ufsDevice;
522 cp = variable_get_value(VAR_UFS_PATH, "Enter a fully qualified pathname for the directory\n"
523 "containing the FreeBSD distribution files:", 0);
525 return DITEM_FAILURE;
527 /* If they gave us a CDROM or something, try and pick a better name */
529 strcpy(ufsDevice.name, "ufs");
531 strcpy(ufsDevice.name, st.f_fstypename);
533 ufsDevice.type = DEVICE_TYPE_UFS;
534 ufsDevice.init = dummyInit;
535 ufsDevice.get = mediaGetUFS;
536 ufsDevice.shutdown = dummyShutdown;
537 ufsDevice.private = strdup(cp);
538 mediaDevice = &ufsDevice;
539 return DITEM_LEAVE_MENU;
543 mediaSetNFS(dialogMenuItem *self)
545 static Device nfsDevice;
546 static Device *networkDev = NULL;
548 char hostname[MAXPATHLEN];
552 cp = variable_get_value(VAR_NFS_PATH, "Please enter the full NFS file specification for the remote\n"
553 "host and directory containing the FreeBSD distribution files.\n"
554 "This should be in the format: hostname:/some/freebsd/dir", 0);
556 return DITEM_FAILURE;
557 SAFE_STRCPY(hostname, cp);
558 if (!(idx = index(hostname, ':'))) {
559 msgConfirm("Invalid NFS path specification. Must be of the form:\n"
560 "host:/full/pathname/to/FreeBSD/distdir");
561 return DITEM_FAILURE;
563 pathlen = strlen(hostname);
564 if (pathlen >= sizeof(nfsDevice.name)) {
565 msgConfirm("Length of specified NFS path is %d characters. Allowable maximum is %d.",
566 pathlen,sizeof(nfsDevice.name)-1);
567 variable_unset(VAR_NFS_PATH);
568 return DITEM_FAILURE;
570 SAFE_STRCPY(nfsDevice.name, hostname);
572 if (!networkDev || msgYesNo("You've already done the network configuration once,\n"
573 "would you like to skip over it now?") != 0) {
575 DEVICE_SHUTDOWN(networkDev);
576 if (!(networkDev = tcpDeviceSelect()))
577 return DITEM_FAILURE;
579 if (!DEVICE_INIT(networkDev)) {
581 msgDebug("mediaSetNFS: Net device init failed\n");
583 if (variable_get(VAR_NAMESERVER)) {
585 if ((inet_addr(hostname) == INADDR_NONE) && (gethostbyname(hostname) == NULL)) {
586 msgConfirm("Cannot resolve hostname `%s'! Are you sure that your\n"
587 "name server, gateway and network interface are correctly configured?", hostname);
589 DEVICE_SHUTDOWN(networkDev);
591 variable_unset(VAR_NFS_PATH);
592 return DITEM_FAILURE;
596 msgDebug("Found DNS entry for %s successfully..\n", hostname);
599 variable_set2(VAR_NFS_HOST, hostname, 0);
600 nfsDevice.type = DEVICE_TYPE_NFS;
601 nfsDevice.init = mediaInitNFS;
602 nfsDevice.get = mediaGetNFS;
603 nfsDevice.shutdown = mediaShutdownNFS;
604 nfsDevice.private = networkDev;
605 mediaDevice = &nfsDevice;
606 return DITEM_LEAVE_MENU;
610 mediaExtractDistBegin(char *dir, int *fd, int *zpid, int *cpid)
612 int i, pfd[2],qfd[2];
622 char *unzipper = RunningAsInit ? "/stand/" UNZIPPER
623 : "/usr/bin/" UNZIPPER;
625 dup2(qfd[0], 0); close(qfd[0]);
626 dup2(pfd[1], 1); close(pfd[1]);
631 open("/dev/null", O_WRONLY);
635 i = execl(unzipper, unzipper, (char *)0);
637 msgDebug("%s command returns %d status\n", unzipper, i);
644 char *cpio = RunningAsInit ? "/stand/cpio" : "/usr/bin/cpio";
646 dup2(pfd[0], 0); close(pfd[0]);
654 close(1); open("/dev/null", O_WRONLY);
657 if (strlen(cpioVerbosity()))
658 i = execl(cpio, cpio, "-idum", cpioVerbosity(), "--block-size", mediaTapeBlocksize(), (char *)0);
660 i = execl(cpio, cpio, "-idum", "--block-size", mediaTapeBlocksize(), (char *)0);
662 msgDebug("%s command returns %d status\n", cpio, i);
671 mediaExtractDistEnd(int zpid, int cpid)
675 i = waitpid(zpid, &j, 0);
676 /* Don't check exit status - gunzip seems to return a bogus one! */
679 msgDebug("wait for %s returned status of %d!\n",
680 USE_GZIP ? "gunzip" : "bunzip2", i);
683 i = waitpid(cpid, &j, 0);
684 if (i < 0 || WEXITSTATUS(j)) {
686 msgDebug("cpio returned error status of %d!\n", WEXITSTATUS(j));
693 mediaExtractDist(char *dir, char *dist, FILE *fp)
695 int i, j, total, seconds, zpid, cpid, pfd[2], qfd[2];
697 struct timeval start, stop;
698 struct sigaction new, old;
705 pipe(pfd); /* read end */
706 pipe(qfd); /* write end */
709 char *unzipper = RunningAsInit ? "/stand/" UNZIPPER
710 : "/usr/bin/" UNZIPPER;
714 dup2(qfd[0], 0); close(qfd[0]);
717 dup2(pfd[1], 1); close(pfd[1]);
723 open("/dev/null", O_WRONLY);
725 i = execl(unzipper, unzipper, (char *)0);
727 msgDebug("%s command returns %d status\n", unzipper, i);
732 char *cpio = RunningAsInit ? "/stand/cpio" : "/usr/bin/cpio";
735 dup2(pfd[0], 0); close(pfd[0]);
736 close (qfd[0]); close(qfd[1]);
743 dup2(open("/dev/null", O_WRONLY), 1);
746 if (strlen(cpioVerbosity()))
747 i = execl(cpio, cpio, "-idum", cpioVerbosity(), "--block-size", mediaTapeBlocksize(), (char *)0);
749 i = execl(cpio, cpio, "-idum", "--block-size", mediaTapeBlocksize(), (char *)0);
751 msgDebug("%s command returns %d status\n", cpio, i);
754 close(pfd[0]); close(pfd[1]);
758 (void)gettimeofday(&start, (struct timezone *)0);
760 /* Make ^C abort the current transfer rather than the whole show */
761 new.sa_handler = handle_intr;
763 (void)sigemptyset(&new.sa_mask);
764 sigaction(SIGINT, &new, &old);
766 while ((i = fread(buf, 1, BUFSIZ, fp)) > 0) {
767 if (check_for_interrupt()) {
768 msgConfirm("Failure to read from media: User interrupt.");
771 if (write(qfd[1], buf, i) != i) {
772 msgConfirm("Write error on transfer to cpio process, try of %d bytes.", i);
776 (void)gettimeofday(&stop, (struct timezone *)0);
777 stop.tv_sec = stop.tv_sec - start.tv_sec;
778 stop.tv_usec = stop.tv_usec - start.tv_usec;
779 if (stop.tv_usec < 0)
780 stop.tv_sec--, stop.tv_usec += 1000000;
781 seconds = stop.tv_sec + (stop.tv_usec / 1000000.0);
785 msgInfo("%10d bytes read from %s dist @ %.1f KB/sec.",
786 total, dist, (total / seconds) / 1024.0);
789 sigaction(SIGINT, &old, NULL); /* restore sigint */
792 i = waitpid(zpid, &j, 0);
793 /* Don't check exit status - gunzip seems to return a bogus one! */
796 msgDebug("wait for %s returned status of %d!\n",
797 USE_GZIP ? "gunzip" : "bunzip2", i);
800 i = waitpid(cpid, &j, 0);
801 if (i < 0 || WEXITSTATUS(j)) {
803 msgDebug("cpio returned error status of %d!\n", WEXITSTATUS(j));
810 mediaGetType(dialogMenuItem *self)
812 return ((dmenuOpenSimple(&MenuMedia, FALSE) && mediaDevice) ? DITEM_SUCCESS : DITEM_FAILURE);
815 /* Return TRUE if all the media variables are set up correctly */
820 return (DITEM_STATUS(mediaGetType(NULL)) == DITEM_SUCCESS);
824 /* Set the FTP username and password fields */
826 mediaSetFTPUserPass(dialogMenuItem *self)
830 if (variable_get_value(VAR_FTP_USER, "Please enter the username you wish to login as:", 0)) {
831 DialogInputAttrs |= DITEM_NO_ECHO;
832 pass = variable_get_value(VAR_FTP_PASS, "Please enter the password for this user:", 0);
833 DialogInputAttrs &= ~DITEM_NO_ECHO;
837 return (pass ? DITEM_SUCCESS : DITEM_FAILURE);
840 /* Set CPIO verbosity level */
842 mediaSetCPIOVerbosity(dialogMenuItem *self)
844 char *cp = variable_get(VAR_CPIO_VERBOSITY);
847 msgConfirm("CPIO Verbosity is not set to anything!");
848 return DITEM_FAILURE;
851 if (!strcmp(cp, "low"))
852 variable_set2(VAR_CPIO_VERBOSITY, "medium", 0);
853 else if (!strcmp(cp, "medium"))
854 variable_set2(VAR_CPIO_VERBOSITY, "high", 0);
855 else /* must be "high" - wrap around */
856 variable_set2(VAR_CPIO_VERBOSITY, "low", 0);
858 return DITEM_SUCCESS;
861 /* A generic open which follows a well-known "path" of places to look */
863 mediaGenericGet(char *base, const char *file)
867 snprintf(buf, PATH_MAX, "%s/%s", base, file);
868 if (file_readable(buf))
869 return fopen(buf, "r");
870 snprintf(buf, PATH_MAX, "%s/FreeBSD/%s", base, file);
871 if (file_readable(buf))
872 return fopen(buf, "r");
873 snprintf(buf, PATH_MAX, "%s/releases/%s", base, file);
874 if (file_readable(buf))
875 return fopen(buf, "r");
876 snprintf(buf, PATH_MAX, "%s/%s/%s", base, variable_get(VAR_RELNAME), file);
877 if (file_readable(buf))
878 return fopen(buf, "r");
879 snprintf(buf, PATH_MAX, "%s/releases/%s/%s", base, variable_get(VAR_RELNAME), file);
880 return fopen(buf, "r");