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