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}