Java:查看目录以移动大文件

时间:2010-07-30 07:17:41

标签: java directory watch

我一直在编写一个监视目录的程序,当在其中创建文件时,它会更改名称并将其移动到新目录。在我的第一个实现中,我使用了Java的Watch Service API,当我测试1kb文件时,它运行良好。出现的问题是,实际上创建的文件是50-300mb。当发生这种情况时,观察者API会立即找到该文件,但由于它仍在被写入,因此无法移动它。我尝试将观察者放在一个循环中(在文件可以移动之前生成异常)但这看起来效率很低。

由于这不起作用,我尝试使用定时器,每隔10秒检查一次文件夹,然后尽可能移动文件。这是我最终选择的方法。

问题:无论如何在没有进行异常检查或不断比较大小的情况下发出文件写入信号?我喜欢为每个文件使用Watcher API一次,而不是使用计时器不断检查(并运行异常)。

非常感谢所有回复!

NT

13 个答案:

答案 0 :(得分:20)

我今天遇到了同样的问题。我在实际导入文件之前使用了一小段延迟并不是一个大问题,我仍然想使用NIO2 API。我选择的解决方案是等待文件在10秒内没有被修改,然后对其执行任何操作。

实施的重要部分如下。程序等待,直到等待时间到期或发生新事件。每次修改文件时都会重置到期时间。如果在等待时间到期之前删除文件,则会从列表中删除该文件。我使用poll方法的预期到期时间超时,即(lastmodified + waitTime)-currentTime

private final Map<Path, Long> expirationTimes = newHashMap();
private Long newFileWait = 10000L;

public void run() {
    for(;;) {
        //Retrieves and removes next watch key, waiting if none are present.
        WatchKey k = watchService.take();

        for(;;) {
            long currentTime = new DateTime().getMillis();

            if(k!=null)
                handleWatchEvents(k);

            handleExpiredWaitTimes(currentTime);

            // If there are no files left stop polling and block on .take()
            if(expirationTimes.isEmpty())
                break;

            long minExpiration = min(expirationTimes.values());
            long timeout = minExpiration-currentTime;
            logger.debug("timeout: "+timeout);
            k = watchService.poll(timeout, TimeUnit.MILLISECONDS);
        }
    }
}

private void handleExpiredWaitTimes(Long currentTime) {
    // Start import for files for which the expirationtime has passed
    for(Entry<Path, Long> entry : expirationTimes.entrySet()) {
        if(entry.getValue()<=currentTime) {
            logger.debug("expired "+entry);
            // do something with the file
            expirationTimes.remove(entry.getKey());
        }
    }
}

private void handleWatchEvents(WatchKey k) {
    List<WatchEvent<?>> events = k.pollEvents();
    for (WatchEvent<?> event : events) {
        handleWatchEvent(event, keys.get(k));
    }
    // reset watch key to allow the key to be reported again by the watch service
    k.reset();
}

private void handleWatchEvent(WatchEvent<?> event, Path dir) throws IOException {
    Kind<?> kind = event.kind();

    WatchEvent<Path> ev = cast(event);
        Path name = ev.context();
        Path child = dir.resolve(name);

    if (kind == ENTRY_MODIFY || kind == ENTRY_CREATE) {
        // Update modified time
        FileTime lastModified = Attributes.readBasicFileAttributes(child, NOFOLLOW_LINKS).lastModifiedTime();
        expirationTimes.put(name, lastModified.toMillis()+newFileWait);
    }

    if (kind == ENTRY_DELETE) {
        expirationTimes.remove(child);
    }
}

答案 1 :(得分:12)

写另一个文件作为原始文件完成的指示。 I.g'fileorg.dat'正在增长,如果完成创建文件'fileorg.done'并检查 仅适用于'fileorg.done'。

使用聪明的命名约定,您不应该遇到问题。

答案 2 :(得分:9)

两种解决方案:

第一个是the answer by stacker的轻微变化:

为不完整的文件使用唯一的前缀。像myhugefile.zip.inc而不是myhugefile.zip之类的东西。上传/创建完成后重命名文件。从手表中排除.inc文件。

第二种是在同一驱动器上使用不同的文件夹来创建/上传/写入文件,并在准备好后将它们移动到监视文件夹。如果它们位于同一个驱动器上,那么移动应该是一个原子操作(我想是依赖于文件系统)。

无论哪种方式,创建文件的客户端都必须做一些额外的工作。

答案 3 :(得分:4)

我知道这是一个老问题,但也许它可以帮助某人。

我有同样的问题,所以我做的是以下内容:

if (kind == ENTRY_CREATE) {
            System.out.println("Creating file: " + child);

            boolean isGrowing = false;
            Long initialWeight = new Long(0);
            Long finalWeight = new Long(0);

            do {
                initialWeight = child.toFile().length();
                Thread.sleep(1000);
                finalWeight = child.toFile().length();
                isGrowing = initialWeight < finalWeight;

            } while(isGrowing);

            System.out.println("Finished creating file!");

        }

创建文件时,文件会变得越来越大。所以我所做的是比较一秒钟分开的重量。应用程序将处于循环中,直到两个权重相同。

答案 4 :(得分:4)

看起来Apache Camel通过尝试重命名文件(java.io.File.renameTo)来处理文件未完成上传的问题。如果重命名失败,则没有读锁定,但继续尝试。当重命名成功时,他们将其重命名,然后继续进行预期的处理。

请参阅下面的 operations.renameFile 。以下是Apache Camel源代码的链接:GenericFileRenameExclusiveReadLockStrategy.javaFileUtil.java

public boolean acquireExclusiveReadLock( ... ) throws Exception {
   LOG.trace("Waiting for exclusive read lock to file: {}", file);

   // the trick is to try to rename the file, if we can rename then we have exclusive read
   // since its a Generic file we cannot use java.nio to get a RW lock
   String newName = file.getFileName() + ".camelExclusiveReadLock";

   // make a copy as result and change its file name
   GenericFile<T> newFile = file.copyFrom(file);
   newFile.changeFileName(newName);
   StopWatch watch = new StopWatch();

   boolean exclusive = false;
   while (!exclusive) {
        // timeout check
        if (timeout > 0) {
            long delta = watch.taken();
            if (delta > timeout) {
                CamelLogger.log(LOG, readLockLoggingLevel,
                        "Cannot acquire read lock within " + timeout + " millis. Will skip the file: " + file);
                // we could not get the lock within the timeout period, so return false
                return false;
            }
        }

        exclusive = operations.renameFile(file.getAbsoluteFilePath(), newFile.getAbsoluteFilePath());
        if (exclusive) {
            LOG.trace("Acquired exclusive read lock to file: {}", file);
            // rename it back so we can read it
            operations.renameFile(newFile.getAbsoluteFilePath(), file.getAbsoluteFilePath());
        } else {
            boolean interrupted = sleep();
            if (interrupted) {
                // we were interrupted while sleeping, we are likely being shutdown so return false
                return false;
            }
        }
   }

   return true;
}

答案 5 :(得分:3)

虽然SO完成复制后无法通过Watcher Service API进行通知,但所有选项似乎都是“解决”(包括这个!)。

如上所述,

1)在UNIX上不能选择移动或复制;

2)如果你有权写,File.canWrite总是返回true,即使文件仍然被复制;

3)等待超时或新事件发生将是一个选项,但是如果系统过载但副本没有完成怎么办?如果超时值很大,程序会等待很长时间。

4)如果您只是在使用文件而不是创建文件,那么写另一个文件以“标记”复制完成不是一个选项。

另一种方法是使用以下代码:

boolean locked = true;

while (locked) {
    RandomAccessFile raf = null;
    try {
            raf = new RandomAccessFile(file, "r"); // it will throw FileNotFoundException. It's not needed to use 'rw' because if the file is delete while copying, 'w' option will create an empty file.
            raf.seek(file.length()); // just to make sure everything was copied, goes to the last byte
            locked = false;
        } catch (IOException e) {
            locked = file.exists();
            if (locked) {
                System.out.println("File locked: '" + file.getAbsolutePath() + "'");
                Thread.sleep(1000); // waits some time
            } else { 
                System.out.println("File was deleted while copying: '" + file.getAbsolutePath() + "'");
            }
    } finally {
            if (raf!=null) {
                raf.close();    
            }
        }
}

答案 6 :(得分:0)

这是一个非常有趣的讨论,因为这肯定是一个面包和黄油用例:等待创建一个新文件,然后以某种方式对文件做出反应。这里的竞争条件很有意思,因为这里的高级要求当然是获取一个事件,然后实际获得(至少)文件的读锁定。对于大型文件或仅仅是大量的文件创建,这可能需要整个工作线程池,它们只是定期尝试获取新创建的文件的锁定,并且当它们成功时,实际上完成工作。但是,正如我确信NT意识到的那样,人们必须小心翼翼地做到这一点,因为它最终是一种轮询,因为它最终是一种轮询方法,而且可扩展性和轮询不是两个完美结合的词。

答案 7 :(得分:0)

当我实现文件系统观察程序来传输上传的文件时,我不得不处理类似的情况。我为解决这个问题而实施的解决方案包括以下内容:

1-首先,维护一个未处理文件的Map(只要文件仍在复制,文件系统就会生成Modify_Event,所以如果标志为false,你可以忽略它们。)

2-在你的fileProcessor中,你从列表中拾取一个文件并检查它是否被文件系统锁定,如果是,你将得到一个异常,只是捕获这个异常并让你的线程处于等待状态(即10秒)然后再次重试,直到锁被释放。处理完文件后,您可以将标志更改为true或将其从地图中删除。

如果在等待时间段内传输同一文件的许多版本,则此解决方案效率不高。

干杯, 拉姆齐

答案 8 :(得分:0)

根据您在写入文件后需要移动文件的紧急程度,您还可以检查稳定的上次修改时间戳,并仅移动文件静止。你需要它保持稳定的时间可能是依赖于实现的,但我认为具有最后修改时间戳的东西在15秒内没有改变应该足够稳定以便移动。

答案 9 :(得分:0)

对于linux中的大文件,文件将被复制,扩展名为.filepart。您只需要使用commons api检查扩展并注册ENTRY_CREATE事件。我使用我的.csv文件(1GB)对其进行了测试并添加了它

public void run()
{
    try
    {
        WatchKey key = myWatcher.take();
        while (key != null)
        {
            for (WatchEvent event : key.pollEvents())
            {
                if (FilenameUtils.isExtension(event.context().toString(), "filepart"))
                {
                    System.out.println("Inside the PartFile " + event.context().toString());
                } else
                {
                    System.out.println("Full file Copied " + event.context().toString());
                    //Do what ever you want to do with this files.
                }
            }
            key.reset();
            key = myWatcher.take();
        }
    } catch (InterruptedException e)
    {
        e.printStackTrace();
    }
}

答案 10 :(得分:0)

如果您无法控制写入过程,请记录所有ENTRY_CREATED个事件,并观察是否有模式

在我的例子中,文件是通过WebDav(Apache)创建的,并且创建了许多临时文件,但同样的文件也触发了两个 ENTRY_CREATED事件。第二个ENTRY_CREATED事件表示复制过程已完成。

以下是我的示例ENTRY_CREATED事件。打印绝对文件路径(您的日志可能会有所不同,具体取决于写入文件的应用程序):

[info] application - /var/www/webdav/.davfs.tmp39dee1 was created
[info] application - /var/www/webdav/document.docx was created
[info] application - /var/www/webdav/.davfs.tmp054fe9 was created
[info] application - /var/www/webdav/document.docx was created
[info] application - /var/www/webdav/.DAV/__db.document.docx was created 

如您所见,我为 document.docx 收到两个ENTRY_CREATED个活动。在第二个事件之后,我知道文件已完成。在我的情况下,临时文件显然被忽略了。

答案 11 :(得分:0)

所以,我遇到了同样的问题并且让以下解决方案适合我。 早先尝试不成功 - 试图监控&#34; lastModifiedTime&#34;每个文件的统计数据,但我注意到大文件的大小增长可能暂停一段时间。(大小不会连续变化)

基本理念 - 对于每个事件,创建一个触发器文件(在临时目录中),其名称格式如下 -

  

OriginalFileName_lastModifiedTime_numberOfTries

此文件为空,所有播放仅在名称中。原始文件仅在通过特定持续时间间隔后才会被考虑,而不会对其进行更改&#34; s&#34;最后修改时间&#34;统计。 (注意 - 因为它是文件统计信息,没有开销 - &gt; O(1))

注意 - 此触发器文件由其他服务处理(例如&#39; FileTrigger &#39;)。

优势 -

  1. 不要睡觉或等待按住系统。
  2. 释放文件观察程序以监控其他事件
  3. FileWatcher的代码 -

    val triggerFileName: String = triggerFileTempDir + orifinalFileName + "_" + Files.getLastModifiedTime(Paths.get(event.getFile.getName.getPath)).toMillis + "_0"
    
    // creates trigger file in temporary directory
    val triggerFile: File = new File(triggerFileName)
    val isCreated: Boolean = triggerFile.createNewFile()
    
    if (isCreated)
        println("Trigger created: " + triggerFileName)
    else
        println("Error in creating trigger file: " + triggerFileName)
    

    FileTrigger的代码(间隔的cron作业说5分钟) -

     val actualPath : String = "Original file directory here"
     val tempPath : String = "Trigger file directory here"
     val folder : File = new File(tempPath)    
     val listOfFiles = folder.listFiles()
    
    for (i <- listOfFiles)
    {
    
        // ActualFileName_LastModifiedTime_NumberOfTries
        val triggerFileName: String = i.getName
        val triggerFilePath: String = i.toString
    
        // extracting file info from trigger file name
        val fileInfo: Array[String] = triggerFileName.split("_", 3)
        // 0 -> Original file name, 1 -> last modified time, 2 -> number of tries
    
        val actualFileName: String = fileInfo(0)
        val actualFilePath: String = actualPath + actualFileName
        val modifiedTime: Long = fileInfo(1).toLong
        val numberOfTries: Int = fileStats(2).toInt
    
        val currentModifiedTime: Long = Files.getLastModifiedTime(Paths.get(actualFilePath)).toMillis
        val differenceInModifiedTimes: Long = currentModifiedTime - modifiedTime
        // checks if file has been copied completely(4 intervals of 5 mins each with no modification)
        if (differenceInModifiedTimes == 0 && numberOfTries == 3)
        {
            FileUtils.deleteQuietly(new File(triggerFilePath))
            println("Trigger file deleted. Original file completed : " + actualFilePath)
        }
        else
        {
            var newTriggerFileName: String = null
            if (differenceInModifiedTimes == 0)
            {
                // updates numberOfTries by 1
                newTriggerFileName = actualFileName + "_" + modifiedTime + "_" + (numberOfTries + 1)
            }
            else
            {
                // updates modified timestamp and resets numberOfTries to 0
                newTriggerFileName = actualFileName + "_" + currentModifiedTime + "_" + 0
            }
    
            // renames trigger file
            new File(triggerFilePath).renameTo(new File(tempPath + newTriggerFileName))
            println("Trigger file renamed: " + triggerFileName + " -> " + newTriggerFileName)
        }    
    }
    

答案 12 :(得分:-1)

我推测java.io.File.canWrite()会告诉你文件写完的时间。