| 1 | // License: GPL. For details, see LICENSE file. |
|---|
| 2 | package org.openstreetmap.josm.data; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 5 | |
|---|
| 6 | import java.io.File; |
|---|
| 7 | import java.io.IOException; |
|---|
| 8 | import java.util.ArrayList; |
|---|
| 9 | import java.util.Date; |
|---|
| 10 | import java.util.Deque; |
|---|
| 11 | import java.util.HashSet; |
|---|
| 12 | import java.util.Iterator; |
|---|
| 13 | import java.util.LinkedList; |
|---|
| 14 | import java.util.List; |
|---|
| 15 | import java.util.Set; |
|---|
| 16 | import java.util.Timer; |
|---|
| 17 | import java.util.TimerTask; |
|---|
| 18 | import java.util.regex.Pattern; |
|---|
| 19 | |
|---|
| 20 | import org.openstreetmap.josm.Main; |
|---|
| 21 | import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask; |
|---|
| 22 | import org.openstreetmap.josm.data.osm.DataSet; |
|---|
| 23 | import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; |
|---|
| 24 | import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter; |
|---|
| 25 | import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener; |
|---|
| 26 | import org.openstreetmap.josm.data.preferences.BooleanProperty; |
|---|
| 27 | import org.openstreetmap.josm.data.preferences.IntegerProperty; |
|---|
| 28 | import org.openstreetmap.josm.gui.MapView; |
|---|
| 29 | import org.openstreetmap.josm.gui.MapView.LayerChangeListener; |
|---|
| 30 | import org.openstreetmap.josm.gui.layer.Layer; |
|---|
| 31 | import org.openstreetmap.josm.gui.layer.OsmDataLayer; |
|---|
| 32 | import org.openstreetmap.josm.io.OsmExporter; |
|---|
| 33 | |
|---|
| 34 | /** |
|---|
| 35 | * Saves data layers periodically so they can be recovered in case of a crash. |
|---|
| 36 | * |
|---|
| 37 | * There are 2 directories |
|---|
| 38 | * - autosave dir: copies of the currently open data layers are saved here every |
|---|
| 39 | * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding |
|---|
| 40 | * files are removed. If this dir is non-empty on start, JOSM assumes |
|---|
| 41 | * that it crashed last time. |
|---|
| 42 | * - deleted layers dir: "secondary archive" - when autosaved layers are restored |
|---|
| 43 | * they are copied to this directory. We cannot keep them in the autosave folder, |
|---|
| 44 | * but just deleting it would be dangerous: Maybe a feature inside the file |
|---|
| 45 | * caused JOSM to crash. If the data is valuable, the user can still try to |
|---|
| 46 | * open with another versions of JOSM or fix the problem manually. |
|---|
| 47 | * |
|---|
| 48 | * The deleted layers dir keeps at most PROP_DELETED_LAYERS files. |
|---|
| 49 | */ |
|---|
| 50 | public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener { |
|---|
| 51 | |
|---|
| 52 | private static final char[] ILLEGAL_CHARACTERS = { '/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':' }; |
|---|
| 53 | private static final String AUTOSAVE_DIR = "autosave"; |
|---|
| 54 | private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers"; |
|---|
| 55 | |
|---|
| 56 | public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true); |
|---|
| 57 | public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1); |
|---|
| 58 | public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5); |
|---|
| 59 | public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60); |
|---|
| 60 | public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000); |
|---|
| 61 | |
|---|
| 62 | private static class AutosaveLayerInfo { |
|---|
| 63 | OsmDataLayer layer; |
|---|
| 64 | String layerName; |
|---|
| 65 | String layerFileName; |
|---|
| 66 | final Deque<File> backupFiles = new LinkedList<File>(); |
|---|
| 67 | } |
|---|
| 68 | |
|---|
| 69 | private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this); |
|---|
| 70 | private Set<DataSet> changedDatasets = new HashSet<DataSet>(); |
|---|
| 71 | private final List<AutosaveLayerInfo> layersInfo = new ArrayList<AutosaveLayerInfo>(); |
|---|
| 72 | private Timer timer; |
|---|
| 73 | private final Object layersLock = new Object(); |
|---|
| 74 | private final Deque<File> deletedLayers = new LinkedList<File>(); |
|---|
| 75 | |
|---|
| 76 | private final File autosaveDir = new File(Main.pref.getPreferencesDir() + AUTOSAVE_DIR); |
|---|
| 77 | private final File deletedLayersDir = new File(Main.pref.getPreferencesDir() + DELETED_LAYERS_DIR); |
|---|
| 78 | |
|---|
| 79 | public void schedule() { |
|---|
| 80 | if (PROP_INTERVAL.get() > 0) { |
|---|
| 81 | |
|---|
| 82 | if (!autosaveDir.exists() && !autosaveDir.mkdirs()) { |
|---|
| 83 | System.out.println(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath())); |
|---|
| 84 | return; |
|---|
| 85 | } |
|---|
| 86 | if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) { |
|---|
| 87 | System.out.println(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath())); |
|---|
| 88 | return; |
|---|
| 89 | } |
|---|
| 90 | |
|---|
| 91 | for (File f: deletedLayersDir.listFiles()) { |
|---|
| 92 | deletedLayers.add(f); // FIXME: sort by mtime |
|---|
| 93 | } |
|---|
| 94 | |
|---|
| 95 | timer = new Timer(true); |
|---|
| 96 | timer.schedule(this, 1000, PROP_INTERVAL.get() * 1000); |
|---|
| 97 | MapView.addLayerChangeListener(this); |
|---|
| 98 | if (Main.isDisplayingMapView()) { |
|---|
| 99 | 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 | } |
|---|