001/*
002 * VM-Operator
003 * Copyright (C) 2023 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.vmoperator.util;
020
021import java.io.IOException;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.List;
025import java.util.Optional;
026
027/**
028 * Utilities to access configurable file system directories. Based on
029 * the [FHS](https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html) and the
030 * [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
031 */
032@SuppressWarnings("PMD.UseUtilityClass")
033public class FsdUtils {
034
035    /**
036     * Adds a directory with the user's name to the path.
037     * If such a directory does not exist yet, creates it.
038     * If this file or the directory is not writable,
039     * return the given path.
040     *
041     * @param path the path
042     * @return the path
043     */
044    public static Path addUser(Path path) {
045        String user = System.getProperty("user.name");
046        if (user == null) {
047            return path;
048        }
049        Path dir = path.resolve(user);
050        if (Files.exists(dir)) {
051            return dir;
052        }
053        try {
054            Files.createDirectories(dir);
055        } catch (IOException e) { // NOPMD
056            // Just trying, doesn't matter
057        }
058        if (!Files.isWritable(dir)) {
059            return path;
060        }
061        return dir;
062    }
063
064    /**
065     * Returns the directory for temporary storage.
066     *
067     * @return the path
068     */
069    public static Path tmpDir() {
070        return Path.of(System.getProperty("java.io.tmpdir", "/tmp"));
071    }
072
073    /**
074     * Returns the real home directory of the user or, if not
075     * available, a sub directory in {@link #tmpDir()} with
076     * the user's name. 
077     *
078     * @return the path
079     */
080    public static Path userHome() {
081        Path home = Optional.ofNullable(System.getProperty("user.home"))
082            .map(Path::of).orElse(null);
083        if (home != null) {
084            return home;
085        }
086        return addUser(tmpDir());
087    }
088
089    /**
090     * Returns the data home.
091     *
092     * @param appName the application name
093     * @return the path
094     */
095    public static Path dataHome(String appName) {
096        return Optional.ofNullable(System.getenv().get("XDG_DATA_HOME"))
097            .map(Path::of).orElse(userHome().resolve(".local").resolve("share"))
098            .resolve(appName);
099    }
100
101    /**
102     * Returns the config home.
103     *
104     * @param appName the application name
105     * @return the path
106     */
107    public static Path configHome(String appName) {
108        return Optional.ofNullable(System.getenv().get("XDG_CONFIG_HOME"))
109            .map(Path::of).orElse(userHome().resolve(".config"))
110            .resolve(appName);
111    }
112
113    /**
114     * Returns the state directory.
115     *
116     * @param appName the application name
117     * @return the path
118     */
119    public static Path stateHome(String appName) {
120        return Optional.ofNullable(System.getenv().get("XDG_STATE_HOME"))
121            .map(Path::of)
122            .orElse(userHome().resolve(".local").resolve("state"))
123            .resolve(appName);
124    }
125
126    /**
127     * Returns the runtime directory.
128     *
129     * @param appName the application name
130     * @return the path
131     */
132    public static Path runtimeDir(String appName) {
133        return Optional.ofNullable(System.getenv("XDG_RUNTIME_DIR"))
134            .map(Path::of).orElseGet(() -> {
135                var runtimeBase = Path.of("/run");
136                var dir = addUser(runtimeBase);
137                if (!dir.equals(runtimeBase)) {
138                    return dir;
139                }
140                return addUser(tmpDir());
141            }).resolve(appName);
142    }
143
144    /**
145     * Find a configuration file. The given filename is searched for in:
146     * 
147     * 1. the current working directory,
148     * 1. the {@link #configHome(String)}
149     * 1. the subdirectory `appName` of `/etc/opt`
150     * 1. the subdirectory `appName` of `/etc`
151     *
152     * @param appName the application name
153     * @param filename the filename
154     * @return the optional
155     */
156    public static Optional<Path> findConfigFile(String appName,
157            String filename) {
158        var candidates = List.of(Path.of(filename),
159            configHome(appName).resolve(filename),
160            Path.of("/etc").resolve("opt").resolve(appName).resolve(filename),
161            Path.of("/etc").resolve(appName).resolve(filename));
162        for (var candidate : candidates) {
163            if (Files.exists(candidate)) {
164                return Optional.of(candidate);
165            }
166        }
167        return Optional.empty();
168    }
169
170}