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}