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