001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu
018 *     jcarsique
019 *     Yannis JULIENNE
020 */
021package org.nuxeo.connect.update.standalone;
022
023import java.io.File;
024import java.io.IOException;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import java.nio.file.attribute.BasicFileAttributes;
028import java.nio.file.attribute.FileTime;
029import java.util.ArrayList;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.Map.Entry;
034import java.util.Random;
035
036import org.apache.commons.io.FileUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.nuxeo.common.Environment;
040import org.nuxeo.common.utils.ZipUtils;
041import org.nuxeo.connect.update.AlreadyExistsPackageException;
042import org.nuxeo.connect.update.LocalPackage;
043import org.nuxeo.connect.update.PackageException;
044import org.nuxeo.connect.update.PackageState;
045import org.nuxeo.connect.update.PackageUpdateService;
046
047/**
048 * The file {@code nxserver/data/packages/.packages} stores the state of all local features.
049 * <p>
050 * Each local package have a corresponding directory in {@code nxserver/data/features/store} which is named:
051 * {@code <package_uid>} ("id-version")
052 *
053 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
054 */
055public class PackagePersistence {
056
057    private static final Log log = LogFactory.getLog(PackagePersistence.class);
058
059    protected final File root;
060
061    protected final File store;
062
063    protected final File temp;
064
065    protected final Random random = new Random();
066
067    protected Map<String, PackageState> states;
068
069    private PackageUpdateService service;
070
071    public PackagePersistence(PackageUpdateService pus) throws IOException {
072        Environment env = Environment.getDefault();
073        root = env.getPath(Environment.NUXEO_MP_DIR, Environment.DEFAULT_MP_DIR);
074        if (!root.isAbsolute()) {
075            throw new RuntimeException();
076        }
077        root.mkdirs();
078        store = new File(root, "store");
079        store.mkdirs();
080        temp = new File(root, "tmp");
081        temp.mkdirs();
082        service = pus;
083        states = loadStates();
084    }
085
086    public File getRoot() {
087        return root;
088    }
089
090    /**
091     * @since 7.1
092     */
093    public File getStore() {
094        return store;
095    }
096
097    public synchronized Map<String, PackageState> getStates() {
098        return new HashMap<>(states);
099    }
100
101    protected Map<String, PackageState> loadStates() throws IOException {
102        Map<String, PackageState> result = new HashMap<>();
103        File file = new File(root, ".packages");
104        if (file.isFile()) {
105            List<String> lines = FileUtils.readLines(file);
106            for (String line : lines) {
107                line = line.trim();
108                if (line.length() == 0 || line.startsWith("#")) {
109                    continue;
110                }
111                int i = line.indexOf('=');
112                String pkgId = line.substring(0, i).trim();
113                String value = line.substring(i + 1).trim();
114                PackageState state = PackageState.getByLabel(value);
115                if (state == PackageState.UNKNOWN) {
116                    try {
117                        // Kept for backward compliance with int instead of enum
118                        state = PackageState.getByValue(value);
119                    } catch (NumberFormatException e) {
120                        // Set as REMOTE if undefined/unreadable
121                        state = PackageState.REMOTE;
122                    }
123                }
124                result.put(pkgId, state);
125            }
126        }
127        return result;
128    }
129
130    protected void writeStates() throws IOException {
131        StringBuilder buf = new StringBuilder();
132        for (Entry<String, PackageState> entry : states.entrySet()) {
133            buf.append(entry.getKey()).append('=').append(entry.getValue()).append("\n");
134        }
135        File file = new File(root, ".packages");
136        FileUtils.writeStringToFile(file, buf.toString());
137    }
138
139    public LocalPackage getPackage(String id) throws PackageException {
140        File file = new File(store, id);
141        if (file.isDirectory()) {
142            return new LocalPackageImpl(file, getState(id), service);
143        }
144        return null;
145    }
146
147    public synchronized LocalPackage addPackage(File file) throws PackageException {
148        if (file.isDirectory()) {
149            return addPackageFromDir(file);
150        } else if (file.isFile()) {
151            File tmp = newTempDir(file.getName());
152            try {
153                ZipUtils.unzip(file, tmp);
154                return addPackageFromDir(tmp);
155            } catch (IOException e) {
156                throw new PackageException("Failed to unzip package: " + file.getName());
157            } finally {
158                // cleanup tmp if exists
159                org.apache.commons.io.FileUtils.deleteQuietly(tmp);
160            }
161        } else {
162            throw new PackageException("Not found: " + file);
163        }
164    }
165
166    /**
167     * Add unzipped packaged to local cache. It replaces SNAPSHOT packages if not installed
168     *
169     * @throws PackageException
170     * @throws AlreadyExistsPackageException If not replacing a SNAPSHOT or if the existing package is installed
171     */
172    protected LocalPackage addPackageFromDir(File file) throws PackageException {
173        LocalPackageImpl pkg = new LocalPackageImpl(file, PackageState.DOWNLOADED, service);
174        File dir = null;
175        try {
176            dir = new File(store, pkg.getId());
177            if (dir.exists()) {
178                LocalPackage oldpkg = getPackage(pkg.getId());
179                if (!pkg.getVersion().isSnapshot()) {
180                    throw new AlreadyExistsPackageException("Package " + pkg.getId() + " already exists");
181                }
182                if (oldpkg.getPackageState().isInstalled()) {
183                    throw new AlreadyExistsPackageException("Package " + pkg.getId() + " is already installed");
184                }
185                log.info(String.format("Replacement of %s in local cache...", oldpkg));
186                org.apache.commons.io.FileUtils.deleteQuietly(dir);
187            }
188            org.apache.commons.io.FileUtils.copyDirectory(file, dir);
189            pkg.getData().setRoot(dir);
190            updateState(pkg.getId(), pkg.state);
191            return pkg;
192        } catch (IOException e) {
193            throw new PackageException(String.format("Failed to move %s to %s", file, dir), e);
194        }
195    }
196
197    public synchronized PackageState getState(String packageId) {
198        PackageState state = states.get(packageId);
199        if (state == null) {
200            return PackageState.REMOTE;
201        }
202        return state;
203    }
204
205    /**
206     * Get the local package having the given name and which is in either one of the following states:
207     * <ul>
208     * <li>{@link PackageState#INSTALLING}
209     * <li>{@link PackageState#INSTALLED}
210     * <li>{@link PackageState#STARTED}
211     * </ul>
212     */
213    public LocalPackage getActivePackage(String name) throws PackageException {
214        String pkgId = getActivePackageId(name);
215        if (pkgId == null) {
216            return null;
217        }
218        return getPackage(pkgId);
219    }
220
221    public synchronized String getActivePackageId(String name) throws PackageException {
222        for (Entry<String, PackageState> entry : states.entrySet()) {
223            String pkgId = entry.getKey();
224            if (pkgId.startsWith(name) && entry.getValue().isInstalled() && getPackage(pkgId).getName().equals(name)) {
225                return pkgId;
226            }
227        }
228        return null;
229    }
230
231    public synchronized List<LocalPackage> getPackages() throws PackageException {
232        File[] list = store.listFiles();
233        if (list != null) {
234            List<LocalPackage> pkgs = new ArrayList<>(list.length);
235            for (File file : list) {
236                if (!file.isDirectory()) {
237                    log.warn("Ignoring file '" + file.getName() + "' in package store");
238                    continue;
239                }
240                pkgs.add(new LocalPackageImpl(file, getState(file.getName()), service));
241            }
242            return pkgs;
243        }
244        return new ArrayList<>();
245    }
246
247    public synchronized void removePackage(String id) throws PackageException {
248        states.remove(id);
249        try {
250            writeStates();
251        } catch (IOException e) {
252            throw new PackageException("Failed to write package states", e);
253        }
254        File file = new File(store, id);
255        org.apache.commons.io.FileUtils.deleteQuietly(file);
256    }
257
258    /**
259     * @deprecated Since 5.7. Use {@link #updateState(String, PackageState)} instead.
260     */
261    @Deprecated
262    public synchronized void updateState(String id, int state) throws PackageException {
263        states.put(id, PackageState.getByValue(state));
264        try {
265            writeStates();
266        } catch (IOException e) {
267            throw new PackageException("Failed to write package states", e);
268        }
269    }
270
271    /**
272     * @since 5.7
273     */
274    public synchronized void updateState(String id, PackageState state) throws PackageException {
275        states.put(id, state);
276        try {
277            writeStates();
278        } catch (IOException e) {
279            throw new PackageException("Failed to write package states", e);
280        }
281    }
282
283    public synchronized void reset() throws PackageException {
284        String[] keys = states.keySet().toArray(new String[states.size()]);
285        for (String key : keys) {
286            states.put(key, PackageState.DOWNLOADED);
287        }
288        try {
289            writeStates();
290        } catch (IOException e) {
291            throw new PackageException("Failed to write package states", e);
292        }
293    }
294
295    protected File newTempDir(String id) {
296        File tmp;
297        synchronized (temp) {
298            do {
299                tmp = new File(temp, id + "-" + random.nextInt());
300            } while (tmp.exists());
301            tmp.mkdirs();
302        }
303        return tmp;
304    }
305
306    /**
307     * @since 5.8
308     */
309    public FileTime getInstallDate(String id) {
310        File file = new File(store, id);
311        if (file.isDirectory()) {
312            Path path = file.toPath();
313            try {
314                FileTime lastModifiedTime = Files.readAttributes(path, BasicFileAttributes.class).lastModifiedTime();
315                return lastModifiedTime;
316            } catch (IOException e) {
317                log.error(e);
318            }
319        }
320        return null;
321    }
322}