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