FileOpUtils { public static void safeRecursiveOverwrite(@NonNull File src, @NonNull File dest, @NonNull FileOp fop, @NonNull ProgressIndicator progress) throws IOException { File destBackup = getTempDir(dest, "backup", fop); boolean success = false; try { if (fop.exists(dest)) { moveOrCopyAndDelete(dest, destBackup, fop, progress); if (fop.exists(dest)) { throw new IOException("Failed to move away or delete existing target file"); } success = true; } } finally { if (!success && fop.exists(destBackup)) { try { recursiveCopy(destBackup, dest, true, fop, progress); fop.deleteFileOrFolder(destBackup); } catch (IOException e) { progress.logWarning(String.format( "Failed to move original content of %s back into place! " + "It should be available at %s", dest, destBackup), e); } } } success = false; try { moveOrCopyAndDelete(src, dest, fop, progress); success = true; } finally { if (!success) { fop.deleteFileOrFolder(dest); if (fop.exists(dest)) { File toDelete = getTempDir(dest, "delete", fop); fop.renameTo(dest, toDelete); fop.deleteOnExit(toDelete); } try { if (fop.exists(dest)) { recursiveCopy(destBackup, dest, true, fop, progress); } else { moveOrCopyAndDelete(destBackup, dest, fop, progress); } } catch (IOException e) { progress.logWarning(String.format( "Failed to move original content of %s back into place! " + "It should be available at %s", dest, destBackup), e); } } } fop.deleteFileOrFolder(destBackup); } private  FileOpUtils(); @NonNull static FileOp create(); static void recursiveCopy(@NonNull File src, @NonNull File dest, @NonNull FileOp fop,
            @NonNull ProgressIndicator progress); static void safeRecursiveOverwrite(@NonNull File src, @NonNull File dest,
            @NonNull FileOp fop, @NonNull ProgressIndicator progress); @Nullable static File getNewTempDir(@NonNull String base, @NonNull FileOp fileOp); @NonNull @VisibleForTesting static File getTempDir(@NonNull String base, int i); @NonNull static File append(@NonNull File base, @NonNull String... segments); @NonNull static File append(@NonNull String base, @NonNull String... segments); @NonNull static String makeRelative(@NonNull File baseDir, @NonNull File toBeRelative, FileOp fop); static void retainTempDirs(Set<File> retain, String base, FileOp mFop);  }
@Test public void safeRecursiveOverwriteSimpleMove() throws Exception { MockFileOp fop = new MockFileOp(); File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); File s3 = new File("/root/src/foo/b"); File s4 = new File("/root/src/foo/bar/a"); File s5 = new File("/root/src/baz/c"); fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(s3.getPath(), "content3"); fop.recordExistingFile(s4.getPath(), "content4"); fop.recordExistingFile(s5.getPath(), "content5"); FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, new FakeProgressIndicator()); assertEquals("content1", new String(fop.getContent(new File("/root/dest/a")))); assertEquals("content2", new String(fop.getContent(new File("/root/dest/foo/a")))); assertEquals("content3", new String(fop.getContent(new File("/root/dest/foo/b")))); assertEquals("content4", new String(fop.getContent(new File("/root/dest/foo/bar/a")))); assertEquals("content5", new String(fop.getContent(new File("/root/dest/baz/c")))); assertEquals(5, fop.getExistingFiles().length); }
@Test public void safeRecursiveOverwriteActuallyOverwrite() throws Exception { MockFileOp fop = new MockFileOp(); File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); File s3 = new File("/root/src/foo/bar/a"); File d1 = new File("/root/dest/a"); File d2 = new File("/root/dest/foo/b"); fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(s3.getPath(), "content3"); fop.recordExistingFile(d1.getPath(), "content4"); fop.recordExistingFile(d2.getPath(), "content5"); FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, new FakeProgressIndicator()); assertEquals("content1", new String(fop.getContent(new File("/root/dest/a")))); assertEquals("content2", new String(fop.getContent(new File("/root/dest/foo/a")))); assertEquals("content3", new String(fop.getContent(new File("/root/dest/foo/bar/a")))); assertEquals(3, fop.getExistingFiles().length); }
@Test public void safeRecursiveOverwriteCantMoveDest() throws Exception { final AtomicBoolean hitRename = new AtomicBoolean(false); MockFileOp fop = new MockFileOp() { @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { if (oldFile.equals(new File("/root/dest"))) { hitRename.set(true); return false; } return super.renameTo(oldFile, newFile); } }; File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); File d1 = new File("/root/dest/b"); fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(d1.getPath(), "content3"); FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, new FakeProgressIndicator()); assertTrue(hitRename.get()); assertEquals("content1", new String(fop.getContent(new File("/root/dest/a")))); assertEquals("content2", new String(fop.getContent(new File("/root/dest/foo/a")))); assertEquals(2, fop.getExistingFiles().length); }
@Test public void safeRecursiveOverwriteCantDeleteDest() throws Exception { MockFileOp fop = new MockFileOp() { @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { if (oldFile.equals(new File("/root/dest"))) { return false; } return super.renameTo(oldFile, newFile); } @Override public boolean delete(@NonNull File oldFile) { if (oldFile.equals(new File("/root/dest"))) { return false; } return super.delete(oldFile); } }; File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); File d1 = new File("/root/dest/b"); fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(d1.getPath(), "content3"); try { FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, new FakeProgressIndicator()); fail("Expected exception"); } catch (Exception expected) {} assertEquals("content1", new String(fop.getContent(s1))); assertEquals("content2", new String(fop.getContent(s2))); assertEquals("content3", new String(fop.getContent(d1))); assertEquals(3, fop.getExistingFiles().length); }
@Test public void safeRecursiveOverwriteCantDeleteDestPartial() throws Exception { AtomicBoolean deletedSomething = new AtomicBoolean(false); MockFileOp fop = new MockFileOp() { @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { if (getAgnosticAbsPath(oldFile).equals("/root/dest")) { return false; } return super.renameTo(oldFile, newFile); } @Override public boolean delete(@NonNull File oldFile) { if (getAgnosticAbsPath(oldFile.getPath()).startsWith(("/root/dest/"))) { if (deletedSomething.compareAndSet(false, true)) { return super.delete(oldFile); } return false; } return super.delete(oldFile); } }; File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); File d1 = new File("/root/dest/b"); File d2 = new File("/root/dest/bar/b"); fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(d1.getPath(), "content3"); fop.recordExistingFile(d2.getPath(), "content4"); try { FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, new FakeProgressIndicator()); fail("Expected exception"); } catch (IOException expected) {} assertTrue(deletedSomething.get()); assertEquals("content1", new String(fop.getContent(s1))); assertEquals("content2", new String(fop.getContent(s2))); assertEquals("content3", new String(fop.getContent(d1))); assertEquals("content4", new String(fop.getContent(d2))); assertEquals(4, fop.getExistingFiles().length); }
@Test public void safeRecursiveOverwriteCantWrite() throws Exception { File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); File d1 = new File("/root/dest/a"); MockFileOp fop = new MockFileOp() { @Override public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { if (source.equals(s1) && dest.equals(d1)) { throw new IOException("failed to copy"); } super.copyFile(source, dest); } @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { if (getAgnosticAbsPath(oldFile.getPath()).equals("/root/src") && getAgnosticAbsPath(newFile.getPath()).equals("/root/dest")) { return false; } return super.renameTo(oldFile, newFile); } }; fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(d1.getPath(), "content3"); try { FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, new FakeProgressIndicator()); fail("Expected exception"); } catch (IOException expected) {} assertEquals("content1", new String(fop.getContent(s1))); assertEquals("content2", new String(fop.getContent(s2))); assertEquals("content3", new String(fop.getContent(d1))); assertEquals(3, fop.getExistingFiles().length); }
@Test public void safeRecursiveOverwriteCantDeleteDestThenCantMoveBack() throws Exception { File d1 = new File("/root/dest/b"); File d2 = new File("/root/dest/foo/b"); MockFileOp fop = new MockFileOp() { @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { if (oldFile.equals(new File("/root/dest"))) { return false; } return super.renameTo(oldFile, newFile); } @Override public boolean delete(@NonNull File oldFile) { if (oldFile.equals(new File("/root/dest"))) { return false; } return super.delete(oldFile); } @Override public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { if (dest.equals(d1)) { throw new IOException("failed to copy"); } super.copyFile(source, dest); } }; File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(d1.getPath(), "content3"); fop.recordExistingFile(d2.getPath(), "content4"); FakeProgressIndicator progress = new FakeProgressIndicator(); try { FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, progress); fail("Expected exception"); } catch (IOException expected) {} final String marker = "available at "; String message = progress.getWarnings().stream() .filter(in -> in.contains(marker)) .findAny() .get(); String backupPath = message .substring(message.indexOf(marker) + marker.length(), message.indexOf('\n')); assertEquals("content3", new String(fop.getContent(new File(backupPath, "b")))); assertEquals("content4", new String(fop.getContent(new File(backupPath, "foo/b")))); }
@Test public void safeRecursiveOverwriteCantCopyThenCantRestore() throws Exception { File d1 = new File("/root/dest/a"); File d2 = new File("/root/dest/foo/b"); File s1 = new File("/root/src/a"); File s2 = new File("/root/src/foo/a"); MockFileOp fop = new MockFileOp() { @Override public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { if (newFile.equals(new File("/root/dest"))) { return false; } return super.renameTo(oldFile, newFile); } @Override public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { if (dest.equals(d1)) { throw new IOException("failed to copy"); } super.copyFile(source, dest); } }; fop.recordExistingFile(s1.getPath(), "content1"); fop.recordExistingFile(s2.getPath(), "content2"); fop.recordExistingFile(d1.getPath(), "content3"); fop.recordExistingFile(d2.getPath(), "content4"); FakeProgressIndicator progress = new FakeProgressIndicator(); try { FileOpUtils.safeRecursiveOverwrite(new File("/root/src/"), new File("/root/dest"), fop, progress); fail("Expected exception"); } catch (IOException expected) {} final String marker = "available at "; String message = progress.getWarnings().stream() .filter(in -> in.contains(marker)) .findAny() .get(); String backupPath = message .substring(message.indexOf(marker) + marker.length(), message.indexOf('\n')); assertEquals("content3", new String(fop.getContent(new File(backupPath, "a")))); assertEquals("content4", new String(fop.getContent(new File(backupPath, "foo/b")))); }