xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/index/PendingFileCompleter.java (revision 7d63a44f8b0d8fed25b0d6b3b3476d9eeaa744f1)
1b5840353SAdam Hornáček /*
2b5840353SAdam Hornáček  * CDDL HEADER START
3b5840353SAdam Hornáček  *
4b5840353SAdam Hornáček  * The contents of this file are subject to the terms of the
5b5840353SAdam Hornáček  * Common Development and Distribution License (the "License").
6b5840353SAdam Hornáček  * You may not use this file except in compliance with the License.
7b5840353SAdam Hornáček  *
8b5840353SAdam Hornáček  * See LICENSE.txt included in this distribution for the specific
9b5840353SAdam Hornáček  * language governing permissions and limitations under the License.
10b5840353SAdam Hornáček  *
11b5840353SAdam Hornáček  * When distributing Covered Code, include this CDDL HEADER in each
12b5840353SAdam Hornáček  * file and include the License file at LICENSE.txt.
13b5840353SAdam Hornáček  * If applicable, add the following below this CDDL HEADER, with the
14b5840353SAdam Hornáček  * fields enclosed by brackets "[]" replaced with your own identifying
15b5840353SAdam Hornáček  * information: Portions Copyright [yyyy] [name of copyright owner]
16b5840353SAdam Hornáček  *
17b5840353SAdam Hornáček  * CDDL HEADER END
18b5840353SAdam Hornáček  */
19b5840353SAdam Hornáček 
20b5840353SAdam Hornáček /*
215d9f3aa0SAdam Hornáček  * Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
22*7d63a44fSVladimir Kotal  * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
23b5840353SAdam Hornáček  */
249805b761SAdam Hornáček package org.opengrok.indexer.index;
25b5840353SAdam Hornáček 
26b5840353SAdam Hornáček import java.io.File;
27b5840353SAdam Hornáček import java.io.IOException;
28b5840353SAdam Hornáček import java.nio.file.DirectoryStream;
29b4b33617SVladimir Kotal import java.nio.file.FileAlreadyExistsException;
30b5840353SAdam Hornáček import java.nio.file.FileVisitResult;
31b5840353SAdam Hornáček import java.nio.file.Files;
32b5840353SAdam Hornáček import java.nio.file.Path;
33b5840353SAdam Hornáček import java.nio.file.Paths;
34b5840353SAdam Hornáček import java.nio.file.SimpleFileVisitor;
35b5840353SAdam Hornáček import java.nio.file.StandardCopyOption;
36b5840353SAdam Hornáček import java.nio.file.attribute.BasicFileAttributes;
371e75da15SVladimir Kotal import java.time.Duration;
381e75da15SVladimir Kotal import java.time.Instant;
39b5840353SAdam Hornáček import java.util.Comparator;
40b5840353SAdam Hornáček import java.util.List;
41b5840353SAdam Hornáček import java.util.Map;
42b5840353SAdam Hornáček import java.util.Set;
43b5840353SAdam Hornáček import java.util.TreeSet;
44*7d63a44fSVladimir Kotal import java.util.concurrent.ConcurrentHashMap;
45b5840353SAdam Hornáček import java.util.logging.Level;
46b5840353SAdam Hornáček import java.util.logging.Logger;
47b5840353SAdam Hornáček import java.util.stream.Collectors;
481e75da15SVladimir Kotal 
499805b761SAdam Hornáček import org.opengrok.indexer.logger.LoggerFactory;
501e75da15SVladimir Kotal import org.opengrok.indexer.util.Progress;
5112fc2901SChris Fraire import org.opengrok.indexer.util.StringUtils;
524da26a1eSChris Fraire import org.opengrok.indexer.util.TandemPath;
53b5840353SAdam Hornáček 
54b5840353SAdam Hornáček /**
55b5840353SAdam Hornáček  * Represents a tracker of pending file deletions and renamings that can later
56b5840353SAdam Hornáček  * be executed.
57b5840353SAdam Hornáček  * <p>
58b5840353SAdam Hornáček  * {@link PendingFileCompleter} is not generally thread-safe, as only
59*7d63a44fSVladimir Kotal  * {@link #add(org.opengrok.indexer.index.PendingFileRenaming)},
60*7d63a44fSVladimir Kotal  * {@link #add(org.opengrok.indexer.index.PendingSymlinkage)} and
61*7d63a44fSVladimir Kotal  * {@link #add(org.opengrok.indexer.index.PendingFileDeletion)} are expected
62*7d63a44fSVladimir Kotal  * to be run in parallel; these methods are thread-safe w.r.t. underlying data structures.
63b5840353SAdam Hornáček  * <p>
64*7d63a44fSVladimir Kotal  * No other methods are thread-safe between each other. E.g.,
65b5840353SAdam Hornáček  * {@link #complete()} should only be called by a single thread after all
66112419efSChris Fraire  * additions of {@link PendingSymlinkage}s, {@link PendingFileDeletion}s, and
67112419efSChris Fraire  * {@link PendingFileRenaming}s are indicated.
68b5840353SAdam Hornáček  */
69b5840353SAdam Hornáček class PendingFileCompleter {
70b5840353SAdam Hornáček 
71b5840353SAdam Hornáček     /**
72b5840353SAdam Hornáček      * An extension that should be used as the suffix of files for
73b5840353SAdam Hornáček      * {@link PendingFileRenaming} actions.
74b5840353SAdam Hornáček      * <p>Value is {@code ".org_opengrok"}.
75b5840353SAdam Hornáček      */
76b5840353SAdam Hornáček     public static final String PENDING_EXTENSION = ".org_opengrok";
77b5840353SAdam Hornáček 
78*7d63a44fSVladimir Kotal     private static final Logger LOGGER = LoggerFactory.getLogger(PendingFileCompleter.class);
79b5840353SAdam Hornáček 
80112419efSChris Fraire     private volatile boolean completing;
81112419efSChris Fraire 
82b5840353SAdam Hornáček     /**
83ff44f24aSAdam Hornáček      * Descending path segment length comparator.
84b5840353SAdam Hornáček      */
85b5840353SAdam Hornáček     private static final Comparator<File> DESC_PATHLEN_COMPARATOR =
86b5840353SAdam Hornáček         (File f1, File f2) -> {
87b5840353SAdam Hornáček             String s1 = f1.getAbsolutePath();
88b5840353SAdam Hornáček             String s2 = f2.getAbsolutePath();
89b5840353SAdam Hornáček             int n1 = countPathSegments(s1);
90b5840353SAdam Hornáček             int n2 = countPathSegments(s2);
91b5840353SAdam Hornáček             // DESC: s2 no. of segments <=> s1 no. of segments
92b5840353SAdam Hornáček             int cmp = Integer.compare(n2, n1);
93a72324b1SAdam Hornáček             if (cmp != 0) {
94a72324b1SAdam Hornáček                 return cmp;
95a72324b1SAdam Hornáček             }
96b5840353SAdam Hornáček 
97b5840353SAdam Hornáček             // the Comparator must also be "consistent with equals", so check
98b5840353SAdam Hornáček             // string contents too when (length)cmp == 0. (ASC: s1 <=> s2.)
99b5840353SAdam Hornáček             cmp = s1.compareTo(s2);
100b5840353SAdam Hornáček             return cmp;
101b5840353SAdam Hornáček     };
102b5840353SAdam Hornáček 
103*7d63a44fSVladimir Kotal     private final Set<PendingFileDeletion> deletions = ConcurrentHashMap.newKeySet();
104b5840353SAdam Hornáček 
105*7d63a44fSVladimir Kotal     private final Set<PendingFileRenaming> renames = ConcurrentHashMap.newKeySet();
106b5840353SAdam Hornáček 
107*7d63a44fSVladimir Kotal     private final Set<PendingSymlinkage> linkages = ConcurrentHashMap.newKeySet();
108b5840353SAdam Hornáček 
109b5840353SAdam Hornáček     /**
110b5840353SAdam Hornáček      * Adds the specified element to this instance's set if it is not already
111b5840353SAdam Hornáček      * present.
112b5840353SAdam Hornáček      * @param e element to be added to this set
113b5840353SAdam Hornáček      * @return {@code true} if this instance's set did not already contain the
114b5840353SAdam Hornáček      * specified element
115112419efSChris Fraire      * @throws IllegalStateException if {@link #complete()} is running
116b5840353SAdam Hornáček      */
add(PendingFileDeletion e)117b5840353SAdam Hornáček     public boolean add(PendingFileDeletion e) {
118112419efSChris Fraire         if (completing) {
119112419efSChris Fraire             throw new IllegalStateException("complete() is running");
120112419efSChris Fraire         }
121b5840353SAdam Hornáček         return deletions.add(e);
122b5840353SAdam Hornáček     }
123b5840353SAdam Hornáček 
124b5840353SAdam Hornáček     /**
125b5840353SAdam Hornáček      * Adds the specified element to this instance's set if it is not already
126b5840353SAdam Hornáček      * present.
127b5840353SAdam Hornáček      * @param e element to be added to this set
128b5840353SAdam Hornáček      * @return {@code true} if this instance's set did not already contain the
129b5840353SAdam Hornáček      * specified element
130112419efSChris Fraire      * @throws IllegalStateException if {@link #complete()} is running
131b5840353SAdam Hornáček      */
add(PendingSymlinkage e)132b5840353SAdam Hornáček     public boolean add(PendingSymlinkage e) {
133112419efSChris Fraire         if (completing) {
134112419efSChris Fraire             throw new IllegalStateException("complete() is running");
135112419efSChris Fraire         }
136b5840353SAdam Hornáček         return linkages.add(e);
137b5840353SAdam Hornáček     }
138b5840353SAdam Hornáček 
139b5840353SAdam Hornáček     /**
140b5840353SAdam Hornáček      * Adds the specified element to this instance's set if it is not already
141*7d63a44fSVladimir Kotal      * present, and also remove any pending deletion for the same absolute path.
142b5840353SAdam Hornáček      * @param e element to be added to this set
143b5840353SAdam Hornáček      * @return {@code true} if this instance's set did not already contain the
144b5840353SAdam Hornáček      * specified element
145112419efSChris Fraire      * @throws IllegalStateException if {@link #complete()} is running
146b5840353SAdam Hornáček      */
add(PendingFileRenaming e)147b5840353SAdam Hornáček     public boolean add(PendingFileRenaming e) {
148112419efSChris Fraire         if (completing) {
149112419efSChris Fraire             throw new IllegalStateException("complete() is running");
150112419efSChris Fraire         }
151b5840353SAdam Hornáček         boolean rc = renames.add(e);
152b5840353SAdam Hornáček         deletions.remove(new PendingFileDeletion(e.getAbsolutePath()));
153b5840353SAdam Hornáček         return rc;
154b5840353SAdam Hornáček     }
155b5840353SAdam Hornáček 
156b5840353SAdam Hornáček     /**
157b5840353SAdam Hornáček      * Complete all the tracked file operations: first in a stage for pending
158112419efSChris Fraire      * deletions, next in a stage for pending renamings, and finally in a stage
159b5840353SAdam Hornáček      * for pending symbolic linkages.
160b5840353SAdam Hornáček      * <p>
161b5840353SAdam Hornáček      * All operations in each stage are tried in parallel, and any failure is
162b5840353SAdam Hornáček      * caught and raises an exception (after all items in the stage have been
163b5840353SAdam Hornáček      * tried).
164b5840353SAdam Hornáček      * <p>
165b5840353SAdam Hornáček      * Deletions are tried for each
166b5840353SAdam Hornáček      * {@link PendingFileDeletion#getAbsolutePath()}; for a version of the path
167b5840353SAdam Hornáček      * with {@link #PENDING_EXTENSION} appended; and also for the path's parent
168b5840353SAdam Hornáček      * directory, which does nothing if the directory is not empty.
169b5840353SAdam Hornáček      * @return the number of successful operations
170b5840353SAdam Hornáček      * @throws java.io.IOException if an I/O error occurs
171b5840353SAdam Hornáček      */
complete()172b5840353SAdam Hornáček     public int complete() throws IOException {
173112419efSChris Fraire         completing = true;
174112419efSChris Fraire         try {
175112419efSChris Fraire             return completeInner();
176112419efSChris Fraire         } finally {
177112419efSChris Fraire             completing = false;
178112419efSChris Fraire         }
179112419efSChris Fraire     }
180112419efSChris Fraire 
completeInner()181112419efSChris Fraire     private int completeInner() throws IOException {
1821e75da15SVladimir Kotal         Instant start = Instant.now();
183b5840353SAdam Hornáček         int numDeletions = completeDeletions();
18412fc2901SChris Fraire         if (LOGGER.isLoggable(Level.FINE)) {
1851e75da15SVladimir Kotal             LOGGER.log(Level.FINE, "deleted {0} file(s) (took {1})",
18612fc2901SChris Fraire                     new Object[] {numDeletions, StringUtils.getReadableTime(
18712fc2901SChris Fraire                             Duration.between(start, Instant.now()).toMillis())});
18812fc2901SChris Fraire         }
1891e75da15SVladimir Kotal 
1901e75da15SVladimir Kotal         start = Instant.now();
191b5840353SAdam Hornáček         int numRenamings = completeRenamings();
19212fc2901SChris Fraire         if (LOGGER.isLoggable(Level.FINE)) {
1931e75da15SVladimir Kotal             LOGGER.log(Level.FINE, "renamed {0} file(s) (took {1})",
19412fc2901SChris Fraire                     new Object[] {numRenamings, StringUtils.getReadableTime(
19512fc2901SChris Fraire                             Duration.between(start, Instant.now()).toMillis())});
19612fc2901SChris Fraire         }
1971e75da15SVladimir Kotal 
1981e75da15SVladimir Kotal         start = Instant.now();
199b5840353SAdam Hornáček         int numLinkages = completeLinkages();
20012fc2901SChris Fraire         if (LOGGER.isLoggable(Level.FINE)) {
2011e75da15SVladimir Kotal             LOGGER.log(Level.FINE, "affirmed links for {0} path(s) (took {1})",
20212fc2901SChris Fraire                     new Object[] {numLinkages, StringUtils.getReadableTime(
20312fc2901SChris Fraire                             Duration.between(start, Instant.now()).toMillis())});
20412fc2901SChris Fraire         }
2051e75da15SVladimir Kotal 
206b5840353SAdam Hornáček         return numDeletions + numRenamings + numLinkages;
207b5840353SAdam Hornáček     }
208b5840353SAdam Hornáček 
209b5840353SAdam Hornáček     /**
210b5840353SAdam Hornáček      * Attempts to rename all the tracked elements, catching any failures, and
211b5840353SAdam Hornáček      * throwing an exception if any failed.
212b5840353SAdam Hornáček      * @return the number of successful renamings
213b5840353SAdam Hornáček      */
completeRenamings()214b5840353SAdam Hornáček     private int completeRenamings() throws IOException {
215b5840353SAdam Hornáček         int numPending = renames.size();
216b5840353SAdam Hornáček         int numFailures = 0;
217b5840353SAdam Hornáček 
218a72324b1SAdam Hornáček         if (numPending < 1) {
219a72324b1SAdam Hornáček             return 0;
220a72324b1SAdam Hornáček         }
221b5840353SAdam Hornáček 
222b5840353SAdam Hornáček         List<PendingFileRenamingExec> pendingExecs = renames.
223b5840353SAdam Hornáček             parallelStream().map(f ->
224b5840353SAdam Hornáček             new PendingFileRenamingExec(f.getTransientPath(),
225b5840353SAdam Hornáček                 f.getAbsolutePath())).collect(
226b5840353SAdam Hornáček             Collectors.toList());
2271e75da15SVladimir Kotal         Map<Boolean, List<PendingFileRenamingExec>> bySuccess;
228b5840353SAdam Hornáček 
2291e75da15SVladimir Kotal         try (Progress progress = new Progress(LOGGER, "pending renames", numPending)) {
2301e75da15SVladimir Kotal             bySuccess = pendingExecs.parallelStream().collect(
231b5840353SAdam Hornáček                             Collectors.groupingByConcurrent((x) -> {
2321e75da15SVladimir Kotal                                 progress.increment();
233b5840353SAdam Hornáček                                 try {
234b5840353SAdam Hornáček                                     doRename(x);
235b5840353SAdam Hornáček                                     return true;
236b5840353SAdam Hornáček                                 } catch (IOException e) {
237b5840353SAdam Hornáček                                     x.exception = e;
238b5840353SAdam Hornáček                                     return false;
239b5840353SAdam Hornáček                                 }
240b5840353SAdam Hornáček                             }));
2411e75da15SVladimir Kotal         }
242b5840353SAdam Hornáček         renames.clear();
243b5840353SAdam Hornáček 
244b5840353SAdam Hornáček         List<PendingFileRenamingExec> failures = bySuccess.getOrDefault(
245b5840353SAdam Hornáček             Boolean.FALSE, null);
246b5840353SAdam Hornáček         if (failures != null && failures.size() > 0) {
247b5840353SAdam Hornáček             numFailures = failures.size();
248b5840353SAdam Hornáček             double pctFailed = 100.0 * numFailures / numPending;
249b5840353SAdam Hornáček             String exmsg = String.format(
250b5840353SAdam Hornáček                 "%d failures (%.1f%%) while renaming pending files",
251b5840353SAdam Hornáček                 numFailures, pctFailed);
252b5840353SAdam Hornáček             throw new IOException(exmsg, failures.get(0).exception);
253b5840353SAdam Hornáček         }
254b5840353SAdam Hornáček 
255b5840353SAdam Hornáček         return numPending - numFailures;
256b5840353SAdam Hornáček     }
257b5840353SAdam Hornáček 
258b5840353SAdam Hornáček     /**
259b5840353SAdam Hornáček      * Attempts to delete all the tracked elements, catching any failures, and
260b5840353SAdam Hornáček      * throwing an exception if any failed.
261b5840353SAdam Hornáček      * @return the number of successful deletions
262b5840353SAdam Hornáček      */
completeDeletions()263b5840353SAdam Hornáček     private int completeDeletions() throws IOException {
264b5840353SAdam Hornáček         int numPending = deletions.size();
265b5840353SAdam Hornáček         int numFailures = 0;
266b5840353SAdam Hornáček 
267a72324b1SAdam Hornáček         if (numPending < 1) {
268a72324b1SAdam Hornáček             return 0;
269a72324b1SAdam Hornáček         }
270b5840353SAdam Hornáček 
271b5840353SAdam Hornáček         List<PendingFileDeletionExec> pendingExecs = deletions.
272b5840353SAdam Hornáček             parallelStream().map(f ->
273b5840353SAdam Hornáček             new PendingFileDeletionExec(f.getAbsolutePath())).collect(
274b5840353SAdam Hornáček             Collectors.toList());
2751e75da15SVladimir Kotal         Map<Boolean, List<PendingFileDeletionExec>> bySuccess;
276b5840353SAdam Hornáček 
2771e75da15SVladimir Kotal         try (Progress progress = new Progress(LOGGER, "pending deletions", numPending)) {
2781e75da15SVladimir Kotal             bySuccess = pendingExecs.parallelStream().collect(
279b5840353SAdam Hornáček                             Collectors.groupingByConcurrent((x) -> {
2801e75da15SVladimir Kotal                                 progress.increment();
281b5840353SAdam Hornáček                                 doDelete(x);
282b5840353SAdam Hornáček                                 return true;
283b5840353SAdam Hornáček                             }));
2841e75da15SVladimir Kotal         }
285b5840353SAdam Hornáček         deletions.clear();
286b5840353SAdam Hornáček 
287b5840353SAdam Hornáček         List<PendingFileDeletionExec> successes = bySuccess.getOrDefault(
288b5840353SAdam Hornáček             Boolean.TRUE, null);
289a72324b1SAdam Hornáček         if (successes != null) {
290a72324b1SAdam Hornáček             tryDeleteParents(successes);
291a72324b1SAdam Hornáček         }
292b5840353SAdam Hornáček 
293b5840353SAdam Hornáček         List<PendingFileDeletionExec> failures = bySuccess.getOrDefault(
294b5840353SAdam Hornáček             Boolean.FALSE, null);
295b5840353SAdam Hornáček         if (failures != null && failures.size() > 0) {
296b5840353SAdam Hornáček             numFailures = failures.size();
297b5840353SAdam Hornáček             double pctFailed = 100.0 * numFailures / numPending;
298b5840353SAdam Hornáček             String exmsg = String.format(
299b5840353SAdam Hornáček                 "%d failures (%.1f%%) while deleting pending files",
300b5840353SAdam Hornáček                 numFailures, pctFailed);
301b5840353SAdam Hornáček             throw new IOException(exmsg, failures.get(0).exception);
302b5840353SAdam Hornáček         }
303b5840353SAdam Hornáček 
304b5840353SAdam Hornáček         return numPending - numFailures;
305b5840353SAdam Hornáček     }
306b5840353SAdam Hornáček 
307b5840353SAdam Hornáček     /**
308b5840353SAdam Hornáček      * Attempts to link the tracked elements, catching any failures, and
309b5840353SAdam Hornáček      * throwing an exception if any failed.
310b5840353SAdam Hornáček      * @return the number of successful linkages
311b5840353SAdam Hornáček      */
completeLinkages()312b5840353SAdam Hornáček     private int completeLinkages() throws IOException {
313b5840353SAdam Hornáček         int numPending = linkages.size();
314b5840353SAdam Hornáček         int numFailures = 0;
315b5840353SAdam Hornáček 
316b5840353SAdam Hornáček         if (numPending < 1) {
317b5840353SAdam Hornáček             return 0;
318b5840353SAdam Hornáček         }
319b5840353SAdam Hornáček 
320b5840353SAdam Hornáček         List<PendingSymlinkageExec> pendingExecs =
321b5840353SAdam Hornáček             linkages.parallelStream().map(f ->
322b5840353SAdam Hornáček                 new PendingSymlinkageExec(f.getSourcePath(),
323b5840353SAdam Hornáček                         f.getTargetRelPath())).collect(Collectors.toList());
324b5840353SAdam Hornáček 
3251e75da15SVladimir Kotal         Map<Boolean, List<PendingSymlinkageExec>> bySuccess;
32632a07ec0SChris Fraire         try (Progress progress = new Progress(LOGGER, "pending linkages", numPending)) {
3271e75da15SVladimir Kotal             bySuccess = pendingExecs.parallelStream().collect(
328b5840353SAdam Hornáček                             Collectors.groupingByConcurrent((x) -> {
3291e75da15SVladimir Kotal                                 progress.increment();
330b5840353SAdam Hornáček                                 try {
331b5840353SAdam Hornáček                                     doLink(x);
332b5840353SAdam Hornáček                                     return true;
333b5840353SAdam Hornáček                                 } catch (IOException e) {
334b5840353SAdam Hornáček                                     x.exception = e;
335b5840353SAdam Hornáček                                     return false;
336b5840353SAdam Hornáček                                 }
337b5840353SAdam Hornáček                             }));
3381e75da15SVladimir Kotal         }
339b5840353SAdam Hornáček         linkages.clear();
340b5840353SAdam Hornáček 
341b5840353SAdam Hornáček         List<PendingSymlinkageExec> failures = bySuccess.getOrDefault(
342b5840353SAdam Hornáček                 Boolean.FALSE, null);
343b5840353SAdam Hornáček         if (failures != null && failures.size() > 0) {
344b5840353SAdam Hornáček             numFailures = failures.size();
345b5840353SAdam Hornáček             double pctFailed = 100.0 * numFailures / numPending;
346b5840353SAdam Hornáček             String exmsg = String.format(
347b5840353SAdam Hornáček                     "%d failures (%.1f%%) while linking pending paths",
348b5840353SAdam Hornáček                     numFailures, pctFailed);
349b5840353SAdam Hornáček             throw new IOException(exmsg, failures.get(0).exception);
350b5840353SAdam Hornáček         }
351b5840353SAdam Hornáček 
352b5840353SAdam Hornáček         return numPending - numFailures;
353b5840353SAdam Hornáček     }
354b5840353SAdam Hornáček 
doDelete(PendingFileDeletionExec del)35520463cfeSChris Fraire     private void doDelete(PendingFileDeletionExec del) {
3564da26a1eSChris Fraire         File f = new File(TandemPath.join(del.absolutePath, PENDING_EXTENSION));
357112419efSChris Fraire         del.absoluteParent = f.getParentFile();
358b5840353SAdam Hornáček 
359b5840353SAdam Hornáček         doDelete(f);
360b5840353SAdam Hornáček         f = new File(del.absolutePath);
361b5840353SAdam Hornáček         doDelete(f);
362b5840353SAdam Hornáček     }
363b5840353SAdam Hornáček 
doDelete(File f)364b5840353SAdam Hornáček     private void doDelete(File f) {
365b5840353SAdam Hornáček         if (f.delete()) {
366b5840353SAdam Hornáček             LOGGER.log(Level.FINER, "Deleted obsolete file: {0}", f.getPath());
367b5840353SAdam Hornáček         } else if (f.exists()) {
368b5840353SAdam Hornáček             LOGGER.log(Level.WARNING, "Failed to delete obsolete file: {0}",
369b5840353SAdam Hornáček                     f.getPath());
370b5840353SAdam Hornáček         }
371b5840353SAdam Hornáček     }
372b5840353SAdam Hornáček 
doRename(PendingFileRenamingExec ren)373b5840353SAdam Hornáček     private void doRename(PendingFileRenamingExec ren) throws IOException {
374b5840353SAdam Hornáček         try {
375b5840353SAdam Hornáček             Files.move(Paths.get(ren.source), Paths.get(ren.target),
376b5840353SAdam Hornáček                 StandardCopyOption.REPLACE_EXISTING);
377b5840353SAdam Hornáček         } catch (IOException e) {
378b5840353SAdam Hornáček             LOGGER.log(Level.WARNING, "Failed to move file: {0} -> {1}",
379b5840353SAdam Hornáček                 new Object[]{ren.source, ren.target});
380b5840353SAdam Hornáček             throw e;
381b5840353SAdam Hornáček         }
382b5840353SAdam Hornáček         if (LOGGER.isLoggable(Level.FINEST)) {
383b5840353SAdam Hornáček             LOGGER.log(Level.FINEST, "Moved pending as file: {0}",
384b5840353SAdam Hornáček                 ren.target);
385b5840353SAdam Hornáček         }
386b5840353SAdam Hornáček     }
387b5840353SAdam Hornáček 
doLink(PendingSymlinkageExec lnk)388b5840353SAdam Hornáček     private void doLink(PendingSymlinkageExec lnk) throws IOException {
389b5840353SAdam Hornáček         try {
390b5840353SAdam Hornáček             if (!needLink(lnk)) {
391b5840353SAdam Hornáček                 return;
392b5840353SAdam Hornáček             }
393b5840353SAdam Hornáček             Path sourcePath = Paths.get(lnk.source);
394b5840353SAdam Hornáček             deleteFileOrDirectory(sourcePath);
3952e3bfc15SAdam Hornáček 
3962e3bfc15SAdam Hornáček             File sourceParentFile = sourcePath.getParent().toFile();
397b4b33617SVladimir Kotal             /*
3982e3bfc15SAdam Hornáček              * The double check-exists in the following conditional is necessary
3992e3bfc15SAdam Hornáček              * because during a race when two threads are simultaneously linking
4002e3bfc15SAdam Hornáček              * for a not-yet-existent `sourceParentFile`, the first check-exists
4012e3bfc15SAdam Hornáček              * will be false for both threads, but then only one will see true
4022e3bfc15SAdam Hornáček              * from mkdirs -- so the other needs a fallback again to
4032e3bfc15SAdam Hornáček              * check-exists.
4042e3bfc15SAdam Hornáček              */
4052e3bfc15SAdam Hornáček             if (sourceParentFile.exists() || sourceParentFile.mkdirs() ||
4062e3bfc15SAdam Hornáček                     sourceParentFile.exists()) {
407b5840353SAdam Hornáček                 Files.createSymbolicLink(sourcePath, Paths.get(lnk.targetRel));
4082e3bfc15SAdam Hornáček             }
409b4b33617SVladimir Kotal         } catch (FileAlreadyExistsException e) {
410b4b33617SVladimir Kotal             // Another case of racing threads. Given that each of them works with the same path,
411b4b33617SVladimir Kotal             // there is no need to worry.
412b4b33617SVladimir Kotal             return;
413b5840353SAdam Hornáček         } catch (IOException e) {
414b5840353SAdam Hornáček             LOGGER.log(Level.WARNING, "Failed to link: {0} -> {1}",
415b5840353SAdam Hornáček                     new Object[]{lnk.source, lnk.targetRel});
416b5840353SAdam Hornáček             throw e;
417b5840353SAdam Hornáček         }
418b4b33617SVladimir Kotal 
419b5840353SAdam Hornáček         if (LOGGER.isLoggable(Level.FINEST)) {
420b5840353SAdam Hornáček             LOGGER.log(Level.FINEST, "Linked pending: {0} -> {1}",
421b5840353SAdam Hornáček                     new Object[]{lnk.source, lnk.targetRel});
422b5840353SAdam Hornáček         }
423b5840353SAdam Hornáček     }
424b5840353SAdam Hornáček 
needLink(PendingSymlinkageExec lnk)425b5840353SAdam Hornáček     private boolean needLink(PendingSymlinkageExec lnk) {
426b5840353SAdam Hornáček         File src = new File(lnk.source);
427b5840353SAdam Hornáček         Path srcpth = src.toPath();
428b5840353SAdam Hornáček         // needed if source doesn't exist or isn't a symlink
429b5840353SAdam Hornáček         if (!src.exists() || !Files.isSymbolicLink(srcpth)) {
430b5840353SAdam Hornáček             return true;
431b5840353SAdam Hornáček         }
432b5840353SAdam Hornáček 
433b5840353SAdam Hornáček         // Re-resolve target.
434b5840353SAdam Hornáček         Path tgtpth = srcpth.getParent().resolve(Paths.get(lnk.targetRel));
435b5840353SAdam Hornáček 
436b5840353SAdam Hornáček         // Re-canonicalize source and target.
437b5840353SAdam Hornáček         String srcCanonical;
438b5840353SAdam Hornáček         String tgtCanonical;
439b5840353SAdam Hornáček         try {
440b5840353SAdam Hornáček             srcCanonical = src.getCanonicalPath();
441b5840353SAdam Hornáček             tgtCanonical = tgtpth.toFile().getCanonicalPath();
442b5840353SAdam Hornáček         } catch (IOException ex) {
443b5840353SAdam Hornáček             return true;
444b5840353SAdam Hornáček         }
445b5840353SAdam Hornáček         // not needed if source's canonical matches re-resolved target canonical
446112419efSChris Fraire         return !tgtCanonical.equals(srcCanonical);
447b5840353SAdam Hornáček     }
448b5840353SAdam Hornáček 
449b5840353SAdam Hornáček     /**
450ff44f24aSAdam Hornáček      * Deletes file or directory recursively.
451b5840353SAdam Hornáček      * <a href="https://stackoverflow.com/questions/779519/delete-directories-recursively-in-java">
452b5840353SAdam Hornáček      * Q: "Delete directories recursively in Java"
453b5840353SAdam Hornáček      * </a>,
454b5840353SAdam Hornáček      * <a href="https://stackoverflow.com/a/779529/933163">
455b5840353SAdam Hornáček      * A: "In Java 7+ you can use {@code Files} class."</a>,
456b5840353SAdam Hornáček      * <a href="https://stackoverflow.com/users/1679995/tomasz-dzi%C4%99cielewski">
457b5840353SAdam Hornáček      * Tomasz Dzięcielewski
458b5840353SAdam Hornáček      * </a>
459b5840353SAdam Hornáček      * @param start the starting file
460b5840353SAdam Hornáček      */
deleteFileOrDirectory(Path start)461b5840353SAdam Hornáček     private void deleteFileOrDirectory(Path start) throws IOException {
462b5840353SAdam Hornáček         if (!start.toFile().exists()) {
463b5840353SAdam Hornáček             return;
464b5840353SAdam Hornáček         }
465c6f0939bSAdam Hornacek         Files.walkFileTree(start, new SimpleFileVisitor<>() {
466b5840353SAdam Hornáček             @Override
467b5840353SAdam Hornáček             public FileVisitResult visitFile(Path file,
468b5840353SAdam Hornáček                     BasicFileAttributes attrs) throws IOException {
469b5840353SAdam Hornáček                 Files.delete(file);
470b5840353SAdam Hornáček                 return FileVisitResult.CONTINUE;
471b5840353SAdam Hornáček             }
472b5840353SAdam Hornáček 
473b5840353SAdam Hornáček             @Override
474b5840353SAdam Hornáček             public FileVisitResult postVisitDirectory(Path dir, IOException exc)
475b5840353SAdam Hornáček                     throws IOException {
476b5840353SAdam Hornáček                 Files.delete(dir);
477b5840353SAdam Hornáček                 return FileVisitResult.CONTINUE;
478b5840353SAdam Hornáček             }
479b5840353SAdam Hornáček         });
480b5840353SAdam Hornáček     }
481b5840353SAdam Hornáček 
482b5840353SAdam Hornáček     /**
483b5840353SAdam Hornáček      * For the unique set of parent directories among
484b5840353SAdam Hornáček      * {@link PendingFileDeletionExec#absoluteParent}, traverse in descending
485b5840353SAdam Hornáček      * order of path-length, and attempt to clean any empty directories.
486b5840353SAdam Hornáček      */
tryDeleteParents(List<PendingFileDeletionExec> dels)487b5840353SAdam Hornáček     private void tryDeleteParents(List<PendingFileDeletionExec> dels) {
488b5840353SAdam Hornáček         Set<File> parents = new TreeSet<>(DESC_PATHLEN_COMPARATOR);
489ff44f24aSAdam Hornáček         dels.forEach((del) -> parents.add(del.absoluteParent));
490b5840353SAdam Hornáček 
491b5840353SAdam Hornáček         SkeletonDirs skels = new SkeletonDirs();
492b5840353SAdam Hornáček         for (File dir : parents) {
493b5840353SAdam Hornáček             skels.reset();
494b5840353SAdam Hornáček             findFilelessChildren(skels, dir);
495112419efSChris Fraire             skels.childDirs.forEach(this::tryDeleteDirectory);
496b5840353SAdam Hornáček             tryDeleteDirectory(dir);
497b5840353SAdam Hornáček         }
498b5840353SAdam Hornáček     }
499b5840353SAdam Hornáček 
tryDeleteDirectory(File dir)500b5840353SAdam Hornáček     private void tryDeleteDirectory(File dir) {
501b5840353SAdam Hornáček         if (dir.delete()) {
502b5840353SAdam Hornáček             LOGGER.log(Level.FINE, "Removed empty parent dir: {0}",
503b5840353SAdam Hornáček                 dir.getAbsolutePath());
504b5840353SAdam Hornáček         }
505b5840353SAdam Hornáček     }
506b5840353SAdam Hornáček 
507b5840353SAdam Hornáček     /**
508b5840353SAdam Hornáček      * Recursively determines eligible, file-less child directories for cleaning
509b5840353SAdam Hornáček      * up, and writes them to {@code skels}.
510b5840353SAdam Hornáček      */
findFilelessChildren(SkeletonDirs skels, File directory)511b5840353SAdam Hornáček     private void findFilelessChildren(SkeletonDirs skels, File directory) {
512a72324b1SAdam Hornáček         if (!directory.exists()) {
513a72324b1SAdam Hornáček             return;
514a72324b1SAdam Hornáček         }
515b5840353SAdam Hornáček         String dirPath = directory.getAbsolutePath();
516b5840353SAdam Hornáček         boolean topLevelIneligible = false;
517b5840353SAdam Hornáček         boolean didLogFileTopLevelIneligible = false;
518b5840353SAdam Hornáček 
519b5840353SAdam Hornáček         try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(
520b5840353SAdam Hornáček             Paths.get(dirPath))) {
521b5840353SAdam Hornáček             for (Path path : directoryStream) {
522b5840353SAdam Hornáček                 File f = path.toFile();
523b5840353SAdam Hornáček                 if (f.isFile()) {
524b5840353SAdam Hornáček                     topLevelIneligible = true;
525b5840353SAdam Hornáček                     if (!didLogFileTopLevelIneligible && LOGGER.isLoggable(
526b5840353SAdam Hornáček                         Level.FINEST)) {
527b5840353SAdam Hornáček                         didLogFileTopLevelIneligible = true; // just once is OK
528b5840353SAdam Hornáček                         LOGGER.log(Level.FINEST, "not file-less due to: {0}",
529b5840353SAdam Hornáček                             f.getAbsolutePath());
530b5840353SAdam Hornáček                     }
531b5840353SAdam Hornáček                 } else {
532b5840353SAdam Hornáček                     findFilelessChildren(skels, f);
533b5840353SAdam Hornáček                     if (!skels.ineligible) {
534b5840353SAdam Hornáček                         skels.childDirs.add(f);
535b5840353SAdam Hornáček                     } else {
536b5840353SAdam Hornáček                         topLevelIneligible = true;
537b5840353SAdam Hornáček                         if (LOGGER.isLoggable(Level.FINEST)) {
538b5840353SAdam Hornáček                             LOGGER.log(Level.FINEST,
539b5840353SAdam Hornáček                                 "its children prevent delete: {0}",
540b5840353SAdam Hornáček                                 f.getAbsolutePath());
541b5840353SAdam Hornáček                         }
542b5840353SAdam Hornáček                     }
543b5840353SAdam Hornáček 
544b5840353SAdam Hornáček                     // Reset this flag so that other potential, eligible
545b5840353SAdam Hornáček                     // children are evaluated.
546b5840353SAdam Hornáček                     skels.ineligible = false;
547b5840353SAdam Hornáček                 }
548b5840353SAdam Hornáček             }
549b5840353SAdam Hornáček         } catch (IOException ex) {
550b5840353SAdam Hornáček             topLevelIneligible = true;
551b5840353SAdam Hornáček             if (LOGGER.isLoggable(Level.FINEST)) {
552b5840353SAdam Hornáček                 LOGGER.log(Level.FINEST, "Failed to stream directory:" +
553b5840353SAdam Hornáček                     directory, ex);
554b5840353SAdam Hornáček             }
555b5840353SAdam Hornáček         }
556b5840353SAdam Hornáček 
557b5840353SAdam Hornáček         skels.ineligible = topLevelIneligible;
558b5840353SAdam Hornáček     }
559b5840353SAdam Hornáček 
560b5840353SAdam Hornáček     /**
561ff44f24aSAdam Hornáček      * Counts segments arising from {@code File.separatorChar} or '\\'.
562d26fc5a1SChris Fraire      * @param path a defined instance
563b5840353SAdam Hornáček      * @return a natural number
564b5840353SAdam Hornáček      */
countPathSegments(String path)565b5840353SAdam Hornáček     private static int countPathSegments(String path) {
566b5840353SAdam Hornáček         int n = 1;
567b5840353SAdam Hornáček         for (int i = 0; i < path.length(); ++i) {
568b5840353SAdam Hornáček             char c = path.charAt(i);
569a72324b1SAdam Hornáček             if (c == File.separatorChar || c == '\\') {
570a72324b1SAdam Hornáček                 ++n;
571a72324b1SAdam Hornáček             }
572b5840353SAdam Hornáček         }
573b5840353SAdam Hornáček         return n;
574b5840353SAdam Hornáček     }
575b5840353SAdam Hornáček 
57620463cfeSChris Fraire     private static class PendingFileDeletionExec {
577112419efSChris Fraire         final String absolutePath;
57820463cfeSChris Fraire         File absoluteParent;
57920463cfeSChris Fraire         IOException exception;
PendingFileDeletionExec(String absolutePath)580d1e826faSAdam Hornáček         PendingFileDeletionExec(String absolutePath) {
581b5840353SAdam Hornáček             this.absolutePath = absolutePath;
582b5840353SAdam Hornáček         }
583b5840353SAdam Hornáček     }
584b5840353SAdam Hornáček 
58520463cfeSChris Fraire     private static class PendingFileRenamingExec {
586112419efSChris Fraire         final String source;
587112419efSChris Fraire         final String target;
58820463cfeSChris Fraire         IOException exception;
PendingFileRenamingExec(String source, String target)589d1e826faSAdam Hornáček         PendingFileRenamingExec(String source, String target) {
590b5840353SAdam Hornáček             this.source = source;
591b5840353SAdam Hornáček             this.target = target;
592b5840353SAdam Hornáček         }
593b5840353SAdam Hornáček     }
594b5840353SAdam Hornáček 
59520463cfeSChris Fraire     private static class PendingSymlinkageExec {
596112419efSChris Fraire         final String source;
597112419efSChris Fraire         final String targetRel;
59820463cfeSChris Fraire         IOException exception;
PendingSymlinkageExec(String source, String relTarget)599d1e826faSAdam Hornáček         PendingSymlinkageExec(String source, String relTarget) {
600b5840353SAdam Hornáček             this.source = source;
601b5840353SAdam Hornáček             this.targetRel = relTarget;
602b5840353SAdam Hornáček         }
603b5840353SAdam Hornáček     }
604b5840353SAdam Hornáček 
605b5840353SAdam Hornáček     /**
606b5840353SAdam Hornáček      * Represents a collection of file-less directories which should also be
607b5840353SAdam Hornáček      * deleted for cleanliness.
608b5840353SAdam Hornáček      */
60920463cfeSChris Fraire     private static class SkeletonDirs {
61020463cfeSChris Fraire         boolean ineligible; // a flag used during recursion
61120463cfeSChris Fraire         final Set<File> childDirs = new TreeSet<>(DESC_PATHLEN_COMPARATOR);
612b5840353SAdam Hornáček 
reset()61320463cfeSChris Fraire         void reset() {
614b5840353SAdam Hornáček             ineligible = false;
615b5840353SAdam Hornáček             childDirs.clear();
616b5840353SAdam Hornáček         }
617b5840353SAdam Hornáček     }
618b5840353SAdam Hornáček }
619