如何在Java中将多个文件原子性地从src复制到dest?

4
在一个需求中,我需要将多个文件从一个位置复制到另一个网络位置。
假设我有以下文件存在于/src位置。 a.pdf,b.pdf,a.doc,b.doc,a.txt和b.txt
我需要将 a.pdf、a.doc 和 a.txt 文件以原子方式一次性复制到 /dest 位置。
目前我正在使用Java.nio.file.Files包和以下代码。
Path srcFile1 = Paths.get("/src/a.pdf");
Path destFile1 = Paths.get("/dest/a.pdf");

Path srcFile2 = Paths.get("/src/a.doc");
Path destFile2 = Paths.get("/dest/a.doc");

Path srcFile3 = Paths.get("/src/a.txt");
Path destFile3 = Paths.get("/dest/a.txt");

Files.copy(srcFile1, destFile1);
Files.copy(srcFile2, destFile2);
Files.copy(srcFile3, destFile3);

但是这种方法文件将一个接一个地被复制。
作为替代方法,为了使整个过程原子化,我考虑将所有文件压缩并移动到/dest,然后在目标位置解压。

这种方法是否正确,可以使整个复制过程成为原子操作?有没有人经历过类似的概念并解决了它。


1
你也可以逐个复制它们,首先使用.tmp文件扩展名,然后重命名它们。但是你的目标是什么? - J. Doe
@J.Doe,逐个复制多个文件不是原子操作。考虑一次性存储多个表的数据事务,我也想在这里实现类似的功能。 - Dhana
我认为没有一种方法可以获得纯粹的原子性,即您保证要么得到完全符合您要求的内容,要么根本不对文件系统进行任何更改。但是,您可以通过像@J.Doe建议的那样做一些接近的事情。我有一个类似的想法,但我的想法是首先将文件复制到您真正想要将它们复制到的目录中的隐藏目录中。然后在复制后将它们移动到正确的位置。您可以相当自信地认为这3个移动操作会成功并且速度很快,但仍然会有短暂的时间只有1或2个文件存在。 - CryptoFool
如果您希望上述情况作为单个事务发生,我建议您使用Java 8中引入的Stream API。首先,在Stream对象中插入每个文件的字节流,并用一个字符分隔,然后将其发送到网络上。到达目标位置后,您可以遍历Stream对象并在相应位置插入每个字节流。 此外,如果您需要维护文件格式(.docx、.pdf、.txt),您应该使用一个Map对象,定义为Map<ByteArrayInputStream, String>,并将此Map对象作为Stream对象发送到网络上。 - Ayush28
你的zip和copy解决方案是正确的,而且是原子性的,所以当出现单个问题时,目标目录中不会出现任何文件。只需确保在临时目录中压缩文件即可。 - Halayem Anis
5个回答

2
这种方法能够使整个复制过程具有原子性吗?有人有类似的经验并解决了它吗?
你可以将文件复制到一个新的临时目录,然后重命名该目录。
在重命名临时目录之前,您需要删除目标目录。
如果目标目录中已经存在其他不想覆盖的文件,则可以将所有文件从临时目录移动到目标目录。
然而,这并不完全是原子性的。
通过删除 /dest:
String tmpPath="/tmp/in/same/partition/as/source";
File tmp=new File(tmpPath);
tmp.mkdirs();
Path srcFile1 = Paths.get("/src/a.pdf");
Path destFile1 = Paths.get(tmpPath+"/dest/a.pdf");

Path srcFile2 = Paths.get("/src/a.doc");
Path destFile2 = Paths.get(tmpPath+"/dest/a.doc");

Path srcFile3 = Paths.get("/src/a.txt");
Path destFile3 = Paths.get(tmpPath+"/dest/a.txt");

Files.copy(srcFile1, destFile1);
Files.copy(srcFile2, destFile2);
Files.copy(srcFile3, destFile3);
delete(new File("/dest"));
tmp.renameTo("/dest");

void delete(File f) throws IOException {
  if (f.isDirectory()) {
    for (File c : f.listFiles())
      delete(c);
  }
  if (!f.delete())
    throw new FileNotFoundException("Failed to delete file: " + f);
}

仅通过覆盖文件:
String tmpPath="/tmp/in/same/partition/as/source";
File tmp=new File(tmpPath);
tmp.mkdirs();
Path srcFile1 = Paths.get("/src/a.pdf");
Path destFile1=paths.get("/dest/a.pdf");
Path tmp1 = Paths.get(tmpPath+"/a.pdf");

Path srcFile2 = Paths.get("/src/a.doc");
Path destFile2=Paths.get("/dest/a.doc");
Path tmp2 = Paths.get(tmpPath+"/a.doc");

Path srcFile3 = Paths.get("/src/a.txt");
Path destFile3=Paths.get("/dest/a.txt");
Path destFile3 = Paths.get(tmpPath+"/a.txt");

Files.copy(srcFile1, tmp1);
Files.copy(srcFile2, tmp2);
Files.copy(srcFile3, tmp3);

//Start of non atomic section(it can be done again if necessary)

Files.deleteIfExists(destFile1);
Files.deleteIfExists(destFile2);
Files.deleteIfExists(destFile2);

Files.move(tmp1,destFile1);
Files.move(tmp2,destFile2);
Files.move(tmp3,destFile3);
//end of non-atomic section

即使第二种方法包含非原子性的部分,复制过程本身使用临时目录,以便文件不被覆盖。
如果在移动文件过程中出现中断,可以轻松完成。
有关移动文件,请参见https://dev59.com/z2445IYBdhLWcg3w5OII#4645271,有关递归删除目录,请参见https://dev59.com/r7nCzYgBFxS5KdRjwR18#779529

2
首先,有几种复制文件或目录的可能性。Baeldung提供了不同可能性的很好的见解。此外,您还可以使用Spring的FileCopyUtils。不幸的是,所有这些方法都不是原子性的。
我找到了一篇旧帖子并对其进行了调整。您可以尝试使用低级别的事务管理支持。这意味着您将从该方法中创建一个事务,并定义在回滚中应该执行什么。Baeldung也有一篇不错的文章。
@Autowired
private PlatformTransactionManager transactionManager;

@Transactional(rollbackOn = IOException.class)
public void copy(List<File> files) throws IOException {
    TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
    TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {

        @Override
        public void afterCompletion(int status) {
            if (status == STATUS_ROLLED_BACK) {
                // try to delete created files
            }
        }
    });

    try {
        // copy files
        transactionManager.commit(transactionStatus);
    } finally {
        transactionManager.rollback(transactionStatus);
    }
}

或者您可以使用简单的 try-catch 块。如果抛出异常,您可以删除已创建的文件。


0

你的问题缺乏原子性的目标。即使是解压文件也从未是原子的,虚拟机可能会在第二个文件块膨胀的中间崩溃并出现OutOfMemoryError。因此,有一个文件完整,第二个文件不完整,第三个文件完全丢失。

我能想到的唯一方法是采用两阶段提交,就像所有建议中都有一个临时目的地突然变成真正的目标一样。这样你就可以确信,第二个操作要么永远不会发生,要么创建最终状态。

另一种方法是在目标后面编写一种便宜的校验和文件。这将使外部进程易于监听此类文件的创建并验证它们的内容与找到的文件。

后者与立即提供容器/ ZIP/ 存档相同,而不是在目录中堆叠文件。大多数存档都具有或支持完整性检查。

(操作系统和文件系统在文件夹被写入时消失的行为也不同。有些接受它并将所有数据写入可恢复缓冲区。其他人仍然接受写入但不改变任何东西。其他人在第一次写入时立即失败,因为设备上的目标块是未知的。)


0

关于原子写入:

标准文件系统没有原子性概念,因此您只需要执行单个操作 - 这将是原子的。

因此,要以原子方式编写更多文件,您需要创建一个带有时间戳名称的文件夹,并将文件复制到该文件夹中。

然后,您可以将其重命名为最终目标或创建符号链接。

您可以使用类似于Linux上的基于文件的卷等任何类似于此的东西。

请记住,删除现有符号链接并创建新链接永远不会是原子的,因此您需要在代码中处理这种情况,并在可用时切换到重命名/链接的文件夹,而不是删除/创建链接。但是,在正常情况下,删除并创建新链接是非常快速的操作。

关于原子读取:

好吧,问题不在代码中,而在操作系统/文件系统级别。

一段时间以前,我遇到了非常相似的情况。有一个运行并同时更改多个文件的数据库引擎。我需要复制当前状态,但第二个文件在第一个文件被复制之前已经更改。

有两种不同的选择: 使用支持快照的文件系统。在某个时刻,您创建一个快照,然后从中复制文件。 您可以使用fsfreeze --freeze锁定文件系统(在Linux上),稍后再使用fsfreeze --unfreeze解锁它。当文件系统被冻结时,您可以像往常一样读取文件,但是没有进程可以更改它们。
对我来说,这些选项都不起作用,因为我无法更改文件系统类型,并且无法锁定文件系统(它是根文件系统)。
我创建了一个空文件,将其挂载为loop文件系统,并格式化它。从那时起,我就可以仅冻结我的虚拟卷而不触及根文件系统。
我的脚本首先调用fsfreeze --freeze /my/volume,然后执行复制操作,然后调用fsfreeze --unfreeze /my/volume。在复制操作期间,文件无法更改,因此复制的文件全部来自同一时刻-对于我的目的,这就像是原子操作。
顺便说一句,一定要确保不要fsfreeze您的根文件系统:-)。我这样做了,重启是唯一的解决方案。

类似数据库的方法:

即使是数据库也不能依赖原子操作,因此它们首先将更改写入WAL(预写式日志)并将其刷新到存储中。一旦刷新完成,它们就可以将更改应用于数据文件。

如果出现任何问题/崩溃,数据库引擎首先加载数据文件并检查WAL中是否有未应用的事务,并最终应用它们。

这也称为日志记录,并且被一些文件系统(ext3、ext4)使用。


0

希望这个解决方案对您有用:根据我的理解,您需要将文件从一个目录复制到另一个目录。 因此,我的解决方案如下: 谢谢!

public class CopyFilesDirectoryProgram {

public static void main(String[] args) throws IOException {
    // TODO Auto-generated method stub
    String sourcedirectoryName="//mention your source path";
    String targetdirectoryName="//mention your destination path";
    File sdir=new File(sourcedirectoryName);
    File tdir=new File(targetdirectoryName);
    //call the method for execution
    abc (sdir,tdir);

}

private static void abc(File sdir, File tdir) throws IOException {
    
    if(sdir.isDirectory()) {
        copyFilesfromDirectory(sdir,tdir);
    }
        else
        {
            Files.copy(sdir.toPath(), tdir.toPath());
        }
    }


private static void copyFilesfromDirectory(File source, File target) throws IOException {
    
    if(!target.exists()) {
        target.mkdir();
        
    }else {
        for(String items:source.list()) {
            abc(new File(source,items),new File(target,items));
        }
    }
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接