001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.data; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.io.File; 007 import java.io.IOException; 008 import java.util.ArrayList; 009 import java.util.Date; 010 import java.util.Deque; 011 import java.util.HashSet; 012 import java.util.Iterator; 013 import java.util.LinkedList; 014 import java.util.List; 015 import java.util.Set; 016 import java.util.Timer; 017 import java.util.TimerTask; 018 import java.util.regex.Pattern; 019 020 import org.openstreetmap.josm.Main; 021 import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask; 022 import org.openstreetmap.josm.data.osm.DataSet; 023 import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 024 import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter; 025 import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener; 026 import org.openstreetmap.josm.data.preferences.BooleanProperty; 027 import org.openstreetmap.josm.data.preferences.IntegerProperty; 028 import org.openstreetmap.josm.gui.MapView; 029 import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 030 import org.openstreetmap.josm.gui.layer.Layer; 031 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 032 import org.openstreetmap.josm.io.OsmExporter; 033 034 /** 035 * Saves data layers periodically so they can be recovered in case of a crash. 036 * 037 * There are 2 directories 038 * - autosave dir: copies of the currently open data layers are saved here every 039 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding 040 * files are removed. If this dir is non-empty on start, JOSM assumes 041 * that it crashed last time. 042 * - deleted layers dir: "secondary archive" - when autosaved layers are restored 043 * they are copied to this directory. We cannot keep them in the autosave folder, 044 * but just deleting it would be dangerous: Maybe a feature inside the file 045 * caused JOSM to crash. If the data is valuable, the user can still try to 046 * open with another versions of JOSM or fix the problem manually. 047 * 048 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files. 049 */ 050 public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener { 051 052 private static final char[] ILLEGAL_CHARACTERS = { '/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':' }; 053 private static final String AUTOSAVE_DIR = "autosave"; 054 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers"; 055 056 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true); 057 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1); 058 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5); 059 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60); 060 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000); 061 062 private static class AutosaveLayerInfo { 063 OsmDataLayer layer; 064 String layerName; 065 String layerFileName; 066 final Deque<File> backupFiles = new LinkedList<File>(); 067 } 068 069 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this); 070 private Set<DataSet> changedDatasets = new HashSet<DataSet>(); 071 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<AutosaveLayerInfo>(); 072 private Timer timer; 073 private final Object layersLock = new Object(); 074 private final Deque<File> deletedLayers = new LinkedList<File>(); 075 076 private final File autosaveDir = new File(Main.pref.getPreferencesDir() + AUTOSAVE_DIR); 077 private final File deletedLayersDir = new File(Main.pref.getPreferencesDir() + DELETED_LAYERS_DIR); 078 079 public void schedule() { 080 if (PROP_INTERVAL.get() > 0) { 081 082 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) { 083 System.out.println(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath())); 084 return; 085 } 086 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) { 087 System.out.println(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath())); 088 return; 089 } 090 091 for (File f: deletedLayersDir.listFiles()) { 092 deletedLayers.add(f); // FIXME: sort by mtime 093 } 094 095 timer = new Timer(true); 096 timer.schedule(this, 1000, PROP_INTERVAL.get() * 1000); 097 MapView.addLayerChangeListener(this); 098 if (Main.isDisplayingMapView()) { 099 for (OsmDataLayer l: Main.map.mapView.getLayersOfType(OsmDataLayer.class)) { 100 registerNewlayer(l); 101 } 102 } 103 } 104 } 105 106 private String getFileName(String layerName, int index) { 107 String result = layerName; 108 for (int i=0; i<ILLEGAL_CHARACTERS.length; i++) { 109 result = result.replaceAll(Pattern.quote(String.valueOf(ILLEGAL_CHARACTERS[i])), 110 '&' + String.valueOf((int)ILLEGAL_CHARACTERS[i]) + ';'); 111 } 112 if (index != 0) { 113 result = result + '_' + index; 114 } 115 return result; 116 } 117 118 private void setLayerFileName(AutosaveLayerInfo layer) { 119 int index = 0; 120 while (true) { 121 String filename = getFileName(layer.layer.getName(), index); 122 boolean foundTheSame = false; 123 for (AutosaveLayerInfo info: layersInfo) { 124 if (info != layer && filename.equals(info.layerFileName)) { 125 foundTheSame = true; 126 break; 127 } 128 } 129 130 if (!foundTheSame) { 131 layer.layerFileName = filename; 132 return; 133 } 134 135 index++; 136 } 137 } 138 139 private File getNewLayerFile(AutosaveLayerInfo layer) { 140 int index = 0; 141 Date now = new Date(); 142 while (true) { 143 File result = new File(autosaveDir, String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%3$s.osm", layer.layerFileName, now, index == 0?"":"_" + index)); 144 try { 145 if (result.createNewFile()) 146 return result; 147 else { 148 System.out.println(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath())); 149 if (index > PROP_INDEX_LIMIT.get()) 150 throw new IOException("index limit exceeded"); 151 } 152 } catch (IOException e) { 153 System.err.println(tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage())); 154 return null; 155 } 156 index++; 157 } 158 } 159 160 private void savelayer(AutosaveLayerInfo info) throws IOException { 161 if (!info.layer.getName().equals(info.layerName)) { 162 setLayerFileName(info); 163 info.layerName = info.layer.getName(); 164 } 165 if (changedDatasets.contains(info.layer.data)) { 166 File file = getNewLayerFile(info); 167 if (file != null) { 168 info.backupFiles.add(file); 169 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */); 170 } 171 } 172 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) { 173 File oldFile = info.backupFiles.remove(); 174 if (!oldFile.delete()) { 175 System.out.println(tr("Unable to delete old backup file {0}", oldFile.getAbsolutePath())); 176 } 177 } 178 } 179 180 @Override 181 public void run() { 182 synchronized (layersLock) { 183 try { 184 for (AutosaveLayerInfo info: layersInfo) { 185 savelayer(info); 186 } 187 changedDatasets.clear(); 188 } catch (Throwable t) { 189 // Don't let exception stop time thread 190 System.err.println("Autosave failed: "); 191 t.printStackTrace(); 192 } 193 } 194 } 195 196 @Override 197 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 198 // Do nothing 199 } 200 201 private void registerNewlayer(OsmDataLayer layer) { 202 synchronized (layersLock) { 203 layer.data.addDataSetListener(datasetAdapter); 204 AutosaveLayerInfo info = new AutosaveLayerInfo(); 205 info.layer = layer; 206 layersInfo.add(info); 207 } 208 } 209 210 @Override 211 public void layerAdded(Layer newLayer) { 212 if (newLayer instanceof OsmDataLayer) { 213 registerNewlayer((OsmDataLayer) newLayer); 214 } 215 } 216 217 @Override 218 public void layerRemoved(Layer oldLayer) { 219 if (oldLayer instanceof OsmDataLayer) { 220 synchronized (layersLock) { 221 OsmDataLayer osmLayer = (OsmDataLayer) oldLayer; 222 osmLayer.data.removeDataSetListener(datasetAdapter); 223 Iterator<AutosaveLayerInfo> it = layersInfo.iterator(); 224 while (it.hasNext()) { 225 AutosaveLayerInfo info = it.next(); 226 if (info.layer == osmLayer) { 227 228 try { 229 savelayer(info); 230 File lastFile = info.backupFiles.pollLast(); 231 if (lastFile != null) { 232 moveToDeletedLayersFolder(lastFile); 233 } 234 for (File file: info.backupFiles) { 235 file.delete(); 236 } 237 } catch (IOException e) { 238 System.err.println(tr("Error while creating backup of removed layer: {0}", e.getMessage())); 239 } 240 241 it.remove(); 242 } 243 } 244 } 245 } 246 } 247 248 @Override 249 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 250 changedDatasets.add(event.getDataset()); 251 } 252 253 public List<File> getUnsavedLayersFiles() { 254 List<File> result = new ArrayList<File>(); 255 File[] files = autosaveDir.listFiles(); 256 if (files == null) 257 return result; 258 for (File file: files) { 259 if (file.isFile()) { 260 result.add(file); 261 } 262 } 263 return result; 264 } 265 266 public void recoverUnsavedLayers() { 267 List<File> files = getUnsavedLayersFiles(); 268 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files")); 269 Main.worker.submit(openFileTsk); 270 Main.worker.submit(new Runnable() { 271 public void run() { 272 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) { 273 moveToDeletedLayersFolder(f); 274 } 275 } 276 }); 277 } 278 279 /** 280 * Move file to the deleted layers directory. 281 * If moving does not work, it will try to delete the file directly. 282 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS, 283 * some files in the deleted layers directory will be removed. 284 * 285 * @param f the file, usually from the autosave dir 286 */ 287 private void moveToDeletedLayersFolder(File f) { 288 File backupFile = new File(deletedLayersDir, f.getName()); 289 290 if (backupFile.exists()) { 291 deletedLayers.remove(backupFile); 292 if (!backupFile.delete()) { 293 System.err.println(String.format("Warning: Could not delete old backup file %s", backupFile)); 294 } 295 } 296 if (f.renameTo(backupFile)) { 297 deletedLayers.add(backupFile); 298 } else { 299 System.err.println(String.format("Warning: Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName())); 300 // we cannot move to deleted folder, so just try to delete it directly 301 if (!f.delete()) { 302 System.err.println(String.format("Warning: Could not delete backup file %s", f)); 303 } 304 } 305 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) { 306 File next = deletedLayers.remove(); 307 if (next == null) { 308 break; 309 } 310 if (!next.delete()) { 311 System.err.println(String.format("Warning: Could not delete archived backup file %s", next)); 312 } 313 } 314 } 315 316 public void dicardUnsavedLayers() { 317 for (File f: getUnsavedLayersFiles()) { 318 moveToDeletedLayersFolder(f); 319 } 320 } 321 }