/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* bstefanescu
* jcarsique
* Yannis JULIENNE
*/
package org.nuxeo.connect.update.standalone;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.Environment;
import org.nuxeo.common.utils.ZipUtils;
import org.nuxeo.connect.update.AlreadyExistsPackageException;
import org.nuxeo.connect.update.LocalPackage;
import org.nuxeo.connect.update.PackageException;
import org.nuxeo.connect.update.PackageState;
import org.nuxeo.connect.update.PackageUpdateService;
/**
* The file {@code nxserver/data/packages/.packages} stores the state of all local features.
* <p>
* Each local package have a corresponding directory in {@code nxserver/data/features/store} which is named:
* {@code <package_uid>} ("id-version")
*
* @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
*/
public class PackagePersistence {
private static final Log log = LogFactory.getLog(PackagePersistence.class);
protected final File root;
protected final File store;
protected final File temp;
protected final Random random = new Random();
protected Map<String, PackageState> states;
private PackageUpdateService service;
public PackagePersistence(PackageUpdateService pus) throws IOException {
Environment env = Environment.getDefault();
root = env.getPath(Environment.NUXEO_MP_DIR, Environment.DEFAULT_MP_DIR);
if (!root.isAbsolute()) {
throw new RuntimeException();
}
root.mkdirs();
store = new File(root, "store");
store.mkdirs();
temp = new File(root, "tmp");
temp.mkdirs();
service = pus;
states = loadStates();
}
public File getRoot() {
return root;
}
/**
* @since 7.1
*/
public File getStore() {
return store;
}
public synchronized Map<String, PackageState> getStates() {
return new HashMap<>(states);
}
protected Map<String, PackageState> loadStates() throws IOException {
Map<String, PackageState> result = new HashMap<>();
File file = new File(root, ".packages");
if (file.isFile()) {
List<String> lines = FileUtils.readLines(file);
for (String line : lines) {
line = line.trim();
if (line.length() == 0 || line.startsWith("#")) {
continue;
}
int i = line.indexOf('=');
String pkgId = line.substring(0, i).trim();
String value = line.substring(i + 1).trim();
PackageState state = PackageState.getByLabel(value);
if (state == PackageState.UNKNOWN) {
try {
// Kept for backward compliance with int instead of enum
state = PackageState.getByValue(value);
} catch (NumberFormatException e) {
// Set as REMOTE if undefined/unreadable
state = PackageState.REMOTE;
}
}
result.put(pkgId, state);
}
}
return result;
}
protected void writeStates() throws IOException {
StringBuilder buf = new StringBuilder();
for (Entry<String, PackageState> entry : states.entrySet()) {
buf.append(entry.getKey()).append('=').append(entry.getValue()).append("\n");
}
File file = new File(root, ".packages");
FileUtils.writeStringToFile(file, buf.toString());
}
public LocalPackage getPackage(String id) throws PackageException {
File file = new File(store, id);
if (file.isDirectory()) {
return new LocalPackageImpl(file, getState(id), service);
}
return null;
}
public synchronized LocalPackage addPackage(File file) throws PackageException {
if (file.isDirectory()) {
return addPackageFromDir(file);
} else if (file.isFile()) {
File tmp = newTempDir(file.getName());
try {
ZipUtils.unzip(file, tmp);
return addPackageFromDir(tmp);
} catch (IOException e) {
throw new PackageException("Failed to unzip package: " + file.getName());
} finally {
// cleanup tmp if exists
org.apache.commons.io.FileUtils.deleteQuietly(tmp);
}
} else {
throw new PackageException("Not found: " + file);
}
}
/**
* Add unzipped packaged to local cache. It replaces SNAPSHOT packages if not installed
*
* @throws PackageException
* @throws AlreadyExistsPackageException If not replacing a SNAPSHOT or if the existing package is installed
*/
protected LocalPackage addPackageFromDir(File file) throws PackageException {
LocalPackageImpl pkg = new LocalPackageImpl(file, PackageState.DOWNLOADED, service);
File dir = null;
try {
dir = new File(store, pkg.getId());
if (dir.exists()) {
LocalPackage oldpkg = getPackage(pkg.getId());
if (!pkg.getVersion().isSnapshot()) {
throw new AlreadyExistsPackageException("Package " + pkg.getId() + " already exists");
}
if (oldpkg.getPackageState().isInstalled()) {
throw new AlreadyExistsPackageException("Package " + pkg.getId() + " is already installed");
}
log.info(String.format("Replacement of %s in local cache...", oldpkg));
org.apache.commons.io.FileUtils.deleteQuietly(dir);
}
org.apache.commons.io.FileUtils.copyDirectory(file, dir);
pkg.getData().setRoot(dir);
updateState(pkg.getId(), pkg.state);
return pkg;
} catch (IOException e) {
throw new PackageException(String.format("Failed to move %s to %s", file, dir), e);
}
}
public synchronized PackageState getState(String packageId) {
PackageState state = states.get(packageId);
if (state == null) {
return PackageState.REMOTE;
}
return state;
}
/**
* Get the local package having the given name and which is in either one of the following states:
* <ul>
* <li>{@link PackageState#INSTALLING}
* <li>{@link PackageState#INSTALLED}
* <li>{@link PackageState#STARTED}
* </ul>
*/
public LocalPackage getActivePackage(String name) throws PackageException {
String pkgId = getActivePackageId(name);
if (pkgId == null) {
return null;
}
return getPackage(pkgId);
}
public synchronized String getActivePackageId(String name) throws PackageException {
for (Entry<String, PackageState> entry : states.entrySet()) {
String pkgId = entry.getKey();
if (pkgId.startsWith(name) && entry.getValue().isInstalled() && getPackage(pkgId).getName().equals(name)) {
return pkgId;
}
}
return null;
}
public synchronized List<LocalPackage> getPackages() throws PackageException {
File[] list = store.listFiles();
if (list != null) {
List<LocalPackage> pkgs = new ArrayList<>(list.length);
for (File file : list) {
if (!file.isDirectory()) {
log.warn("Ignoring file '" + file.getName() + "' in package store");
continue;
}
pkgs.add(new LocalPackageImpl(file, getState(file.getName()), service));
}
return pkgs;
}
return new ArrayList<>();
}
public synchronized void removePackage(String id) throws PackageException {
states.remove(id);
try {
writeStates();
} catch (IOException e) {
throw new PackageException("Failed to write package states", e);
}
File file = new File(store, id);
org.apache.commons.io.FileUtils.deleteQuietly(file);
}
/**
* @deprecated Since 5.7. Use {@link #updateState(String, PackageState)} instead.
*/
@Deprecated
public synchronized void updateState(String id, int state) throws PackageException {
states.put(id, PackageState.getByValue(state));
try {
writeStates();
} catch (IOException e) {
throw new PackageException("Failed to write package states", e);
}
}
/**
* @since 5.7
*/
public synchronized void updateState(String id, PackageState state) throws PackageException {
states.put(id, state);
try {
writeStates();
} catch (IOException e) {
throw new PackageException("Failed to write package states", e);
}
}
public synchronized void reset() throws PackageException {
String[] keys = states.keySet().toArray(new String[states.size()]);
for (String key : keys) {
states.put(key, PackageState.DOWNLOADED);
}
try {
writeStates();
} catch (IOException e) {
throw new PackageException("Failed to write package states", e);
}
}
protected File newTempDir(String id) {
File tmp;
synchronized (temp) {
do {
tmp = new File(temp, id + "-" + random.nextInt());
} while (tmp.exists());
tmp.mkdirs();
}
return tmp;
}
/**
* @since 5.8
*/
public FileTime getInstallDate(String id) {
File file = new File(store, id);
if (file.isDirectory()) {
Path path = file.toPath();
try {
FileTime lastModifiedTime = Files.readAttributes(path, BasicFileAttributes.class).lastModifiedTime();
return lastModifiedTime;
} catch (IOException e) {
log.error(e);
}
}
return null;
}
}