source: josm/trunk/src/org/openstreetmap/josm/data/AutosaveTask.java@ 9853

Last change on this file since 9853 was 9586, checked in by Don-vip, 8 years ago

fix #12374 - compression option for autosave (patch by kolesar)

  • Property svn:eol-style set to native
File size: 15.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.BufferedReader;
8import java.io.File;
9import java.io.FileFilter;
10import java.io.IOException;
11import java.io.PrintStream;
12import java.lang.management.ManagementFactory;
13import java.nio.charset.StandardCharsets;
14import java.nio.file.Files;
15import java.util.ArrayList;
16import java.util.Date;
17import java.util.Deque;
18import java.util.HashSet;
19import java.util.Iterator;
20import java.util.LinkedList;
21import java.util.List;
22import java.util.Set;
23import java.util.Timer;
24import java.util.TimerTask;
25import java.util.regex.Pattern;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask;
29import org.openstreetmap.josm.data.osm.DataSet;
30import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
31import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
32import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
33import org.openstreetmap.josm.data.preferences.BooleanProperty;
34import org.openstreetmap.josm.data.preferences.IntegerProperty;
35import org.openstreetmap.josm.gui.MapView;
36import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
37import org.openstreetmap.josm.gui.Notification;
38import org.openstreetmap.josm.gui.layer.Layer;
39import org.openstreetmap.josm.gui.layer.OsmDataLayer;
40import org.openstreetmap.josm.gui.util.GuiHelper;
41import org.openstreetmap.josm.io.OsmExporter;
42import org.openstreetmap.josm.io.OsmImporter;
43import org.openstreetmap.josm.tools.Utils;
44
45/**
46 * Saves data layers periodically so they can be recovered in case of a crash.
47 *
48 * There are 2 directories
49 * - autosave dir: copies of the currently open data layers are saved here every
50 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding
51 * files are removed. If this dir is non-empty on start, JOSM assumes
52 * that it crashed last time.
53 * - deleted layers dir: "secondary archive" - when autosaved layers are restored
54 * they are copied to this directory. We cannot keep them in the autosave folder,
55 * but just deleting it would be dangerous: Maybe a feature inside the file
56 * caused JOSM to crash. If the data is valuable, the user can still try to
57 * open with another versions of JOSM or fix the problem manually.
58 *
59 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files.
60 */
61public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener {
62
63 private static final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
64 private static final String AUTOSAVE_DIR = "autosave";
65 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
66
67 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
68 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
69 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
70 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60);
71 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
72 /** Defines if a notification should be displayed after each autosave */
73 public static final BooleanProperty PROP_NOTIFICATION = new BooleanProperty("autosave.notification", false);
74
75 private static class AutosaveLayerInfo {
76 private OsmDataLayer layer;
77 private String layerName;
78 private String layerFileName;
79 private final Deque<File> backupFiles = new LinkedList<>();
80 }
81
82 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
83 private final Set<DataSet> changedDatasets = new HashSet<>();
84 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<>();
85 private Timer timer;
86 private final Object layersLock = new Object();
87 private final Deque<File> deletedLayers = new LinkedList<>();
88
89 private final File autosaveDir = new File(Main.pref.getUserDataDirectory(), AUTOSAVE_DIR);
90 private final File deletedLayersDir = new File(Main.pref.getUserDataDirectory(), DELETED_LAYERS_DIR);
91
92 public void schedule() {
93 if (PROP_INTERVAL.get() > 0) {
94
95 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
96 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
97 return;
98 }
99 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
100 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
101 return;
102 }
103
104 File[] files = deletedLayersDir.listFiles();
105 if (files != null) {
106 for (File f: files) {
107 deletedLayers.add(f); // FIXME: sort by mtime
108 }
109 }
110
111 timer = new Timer(true);
112 timer.schedule(this, 1000L, PROP_INTERVAL.get() * 1000L);
113 MapView.addLayerChangeListener(this);
114 if (Main.isDisplayingMapView()) {
115 for (OsmDataLayer l: Main.map.mapView.getLayersOfType(OsmDataLayer.class)) {
116 registerNewlayer(l);
117 }
118 }
119 }
120 }
121
122 private static String getFileName(String layerName, int index) {
123 String result = layerName;
124 for (char illegalCharacter : ILLEGAL_CHARACTERS) {
125 result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)),
126 '&' + String.valueOf((int) illegalCharacter) + ';');
127 }
128 if (index != 0) {
129 result = result + '_' + index;
130 }
131 return result;
132 }
133
134 private void setLayerFileName(AutosaveLayerInfo layer) {
135 int index = 0;
136 while (true) {
137 String filename = getFileName(layer.layer.getName(), index);
138 boolean foundTheSame = false;
139 for (AutosaveLayerInfo info: layersInfo) {
140 if (info != layer && filename.equals(info.layerFileName)) {
141 foundTheSame = true;
142 break;
143 }
144 }
145
146 if (!foundTheSame) {
147 layer.layerFileName = filename;
148 return;
149 }
150
151 index++;
152 }
153 }
154
155 private File getNewLayerFile(AutosaveLayerInfo layer) {
156 int index = 0;
157 Date now = new Date();
158 while (true) {
159 String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s",
160 layer.layerFileName, now, index == 0 ? "" : '_' + index);
161 File result = new File(autosaveDir, filename + "." + Main.pref.get("autosave.extension", "osm"));
162 try {
163 if (result.createNewFile()) {
164 File pidFile = new File(autosaveDir, filename+".pid");
165 try (PrintStream ps = new PrintStream(pidFile, "UTF-8")) {
166 ps.println(ManagementFactory.getRuntimeMXBean().getName());
167 } catch (Exception t) {
168 Main.error(t);
169 }
170 return result;
171 } else {
172 Main.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
173 if (index > PROP_INDEX_LIMIT.get())
174 throw new IOException("index limit exceeded");
175 }
176 } catch (IOException e) {
177 Main.error(tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()));
178 return null;
179 }
180 index++;
181 }
182 }
183
184 private void savelayer(AutosaveLayerInfo info) {
185 if (!info.layer.getName().equals(info.layerName)) {
186 setLayerFileName(info);
187 info.layerName = info.layer.getName();
188 }
189 if (changedDatasets.remove(info.layer.data)) {
190 File file = getNewLayerFile(info);
191 if (file != null) {
192 info.backupFiles.add(file);
193 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */);
194 }
195 }
196 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
197 File oldFile = info.backupFiles.remove();
198 if (Utils.deleteFile(oldFile, marktr("Unable to delete old backup file {0}"))) {
199 Utils.deleteFile(getPidFile(oldFile), marktr("Unable to delete old backup file {0}"));
200 }
201 }
202 }
203
204 @Override
205 public void run() {
206 synchronized (layersLock) {
207 try {
208 for (AutosaveLayerInfo info: layersInfo) {
209 savelayer(info);
210 }
211 changedDatasets.clear();
212 if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) {
213 displayNotification();
214 }
215 } catch (Exception t) {
216 // Don't let exception stop time thread
217 Main.error("Autosave failed:");
218 Main.error(t);
219 }
220 }
221 }
222
223 protected void displayNotification() {
224 GuiHelper.runInEDT(new Runnable() {
225 @Override
226 public void run() {
227 new Notification(tr("Your work has been saved automatically."))
228 .setDuration(Notification.TIME_SHORT)
229 .show();
230 }
231 });
232 }
233
234 @Override
235 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
236 // Do nothing
237 }
238
239 private void registerNewlayer(OsmDataLayer layer) {
240 synchronized (layersLock) {
241 layer.data.addDataSetListener(datasetAdapter);
242 AutosaveLayerInfo info = new AutosaveLayerInfo();
243 info.layer = layer;
244 layersInfo.add(info);
245 }
246 }
247
248 @Override
249 public void layerAdded(Layer newLayer) {
250 if (newLayer instanceof OsmDataLayer) {
251 registerNewlayer((OsmDataLayer) newLayer);
252 }
253 }
254
255 @Override
256 public void layerRemoved(Layer oldLayer) {
257 if (oldLayer instanceof OsmDataLayer) {
258 synchronized (layersLock) {
259 OsmDataLayer osmLayer = (OsmDataLayer) oldLayer;
260 osmLayer.data.removeDataSetListener(datasetAdapter);
261 Iterator<AutosaveLayerInfo> it = layersInfo.iterator();
262 while (it.hasNext()) {
263 AutosaveLayerInfo info = it.next();
264 if (info.layer == osmLayer) {
265
266 savelayer(info);
267 File lastFile = info.backupFiles.pollLast();
268 if (lastFile != null) {
269 moveToDeletedLayersFolder(lastFile);
270 }
271 for (File file: info.backupFiles) {
272 if (Utils.deleteFile(file)) {
273 Utils.deleteFile(getPidFile(file));
274 }
275 }
276
277 it.remove();
278 }
279 }
280 }
281 }
282 }
283
284 @Override
285 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
286 changedDatasets.add(event.getDataset());
287 }
288
289 private File getPidFile(File osmFile) {
290 return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid"));
291 }
292
293 /**
294 * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM.
295 * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance.
296 * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM
297 */
298 public List<File> getUnsavedLayersFiles() {
299 List<File> result = new ArrayList<>();
300 File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER);
301 if (files == null)
302 return result;
303 for (File file: files) {
304 if (file.isFile()) {
305 boolean skipFile = false;
306 File pidFile = getPidFile(file);
307 if (pidFile.exists()) {
308 try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) {
309 String jvmId = reader.readLine();
310 if (jvmId != null) {
311 String pid = jvmId.split("@")[0];
312 skipFile = jvmPerfDataFileExists(pid);
313 }
314 } catch (Exception t) {
315 Main.error(t);
316 }
317 }
318 if (!skipFile) {
319 result.add(file);
320 }
321 }
322 }
323 return result;
324 }
325
326 private boolean jvmPerfDataFileExists(final String jvmId) {
327 File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name"));
328 if (jvmDir.exists() && jvmDir.canRead()) {
329 File[] files = jvmDir.listFiles(new FileFilter() {
330 @Override
331 public boolean accept(File file) {
332 return file.getName().equals(jvmId) && file.isFile();
333 }
334 });
335 return files != null && files.length == 1;
336 }
337 return false;
338 }
339
340 public void recoverUnsavedLayers() {
341 List<File> files = getUnsavedLayersFiles();
342 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
343 Main.worker.submit(openFileTsk);
344 Main.worker.submit(new Runnable() {
345 @Override
346 public void run() {
347 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
348 moveToDeletedLayersFolder(f);
349 }
350 }
351 });
352 }
353
354 /**
355 * Move file to the deleted layers directory.
356 * If moving does not work, it will try to delete the file directly.
357 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS,
358 * some files in the deleted layers directory will be removed.
359 *
360 * @param f the file, usually from the autosave dir
361 */
362 private void moveToDeletedLayersFolder(File f) {
363 File backupFile = new File(deletedLayersDir, f.getName());
364 File pidFile = getPidFile(f);
365
366 if (backupFile.exists()) {
367 deletedLayers.remove(backupFile);
368 Utils.deleteFile(backupFile, marktr("Unable to delete old backup file {0}"));
369 }
370 if (f.renameTo(backupFile)) {
371 deletedLayers.add(backupFile);
372 Utils.deleteFile(pidFile);
373 } else {
374 Main.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
375 // we cannot move to deleted folder, so just try to delete it directly
376 if (Utils.deleteFile(f, marktr("Unable to delete backup file {0}"))) {
377 Utils.deleteFile(pidFile, marktr("Unable to delete PID file {0}"));
378 }
379 }
380 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
381 File next = deletedLayers.remove();
382 if (next == null) {
383 break;
384 }
385 Utils.deleteFile(next, marktr("Unable to delete archived backup file {0}"));
386 }
387 }
388
389 public void discardUnsavedLayers() {
390 for (File f: getUnsavedLayersFiles()) {
391 moveToDeletedLayersFolder(f);
392 }
393 }
394}
Note: See TracBrowser for help on using the repository browser.