web-dev-qa-db-ja.com

Java:ディレクトリを監視して大きなファイルを移動する

私はディレクトリを監視するプログラムを書いていて、その中にファイルが作成されると、名前が変更されて新しいディレクトリに移動します。最初の実装では、JavaのWatch Service APIを使用しました。これは、1kbのファイルをテストするときに正常に機能しました。発生した問題は、実際に作成されるファイルが50〜300MBの範囲にあることです。これが発生すると、ウォッチャーAPIはファイルをすぐに見つけますが、まだ書き込まれているため、ファイルを移動できませんでした。ウォッチャーをループに入れてみましたが(ファイルを移動できるようになるまで例外が発生しました)、これはかなり非効率的でした。

それがうまくいかなかったので、10秒ごとにフォルダーをチェックし、可能な場合はファイルを移動するタイマーを使用してみました。これが私がやった方法です。

質問:例外チェックを実行したり、サイズを継続的に比較したりせずに、ファイルの書き込みが完了したことを通知する方法はありますか?タイマーで継続的にチェックする(そして例外が発生する)のではなく、ファイルごとに1回だけWatcherAPIを使用するというアイデアが好きです。

すべての回答は大歓迎です!

nt

28
nite

元のファイルが完成したことを示すために、別のファイルを書き込みます。たとえば、ファイル「fileorg.done」を作成し、「fileorg.done」のみをチェックすると、「fileorg.dat」が大きくなります。

巧妙な命名規則を使用すれば、問題は発生しないはずです。

12
stacker

今日も同じ問題に遭遇しました。私のユースケースでは、ファイルが実際にインポートされるまでのわずかな遅延は大きな問題ではなく、それでもNIO2APIを使用したかったのです。私が選んだ解決策は、ファイルが変更されなくなるまで10秒間待ってから、操作を実行することでした。

実装の重要な部分は次のとおりです。プログラムは、待機時間が経過するか、新しいイベントが発生するまで待機します。有効期限は、ファイルが変更されるたびにリセットされます。待機時間が経過する前にファイルが削除されると、そのファイルはリストから削除されます。予想される有効期限のタイムアウト、つまり(lastmodified + waitTime)-currentTimeでpollメソッドを使用します

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);
    }
}
20

2つの解決策:

1つ目は、 スタッカーによる回答 のわずかなバリエーションです。

不完全なファイルには一意のプレフィックスを使用します。何かのようなもの myhugefile.Zip.inc の代わりに myhugefile.Zip。アップロード/作成が終了したら、ファイルの名前を変更します。 .incファイルを時計から除外します。

2つ目は、同じドライブ上の別のフォルダーを使用してファイルを作成/アップロード/書き込みし、準備ができたら監視フォルダーに移動することです。それらが同じドライブ上にある場合、移動はアトミックアクションである必要があります(ファイルシステムに依存すると思います)。

いずれにせよ、ファイルを作成するクライアントはいくつかの追加作業を行う必要があります。

9

私はそれが古い質問であることを知っていますが、多分それは誰かを助けることができます。

私は同じ問題を抱えていたので、私がしたことは次のとおりでした:

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!");

        }

ファイルが作成されると、ファイルはどんどん大きくなります。だから私がしたことは、1秒間隔で重量を比較することでした。両方の重みが同じになるまで、アプリはループになります。

4
user1322265

Apache Camelは、ファイルの名前を変更しようとすることで、ファイルのアップロードが行われない問題を処理しているようです(Java.io.File.renameTo)。名前の変更が失敗した場合、読み取りロックはありませんが、試行を続けてください。名前の変更が成功すると、名前を元に戻し、目的の処理に進みます。

以下のoperations.renameFileを参照してください。 Apache Camelソースへのリンクは次のとおりです。 GenericFileRenameExclusiveReadLockStrategy.Java および FileUtil.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;
}
4
Flint O'Brien

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();    
            }
        }
}
3

書き込みプロセスを制御できない場合は、すべてをログに記録しますENTRY_CREATEDイベントを実行し、パターンがあるかどうかを確認します。

私の場合、ファイルはWebDav(Apache)を介して作成され、多くの一時ファイルが作成されますが、twoENTRY_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 

ご覧のとおり、2つのENTRY_CREATEDイベントdocument.docx。 2番目のイベントの後、ファイルが完成したことがわかります。私の場合、一時ファイルは明らかに無視されます。

0
enigma969

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();
    }
}
0
Pawan Kumar

これは非常に興味深いの議論です。確かにこれはパンとバターのユースケースです。新しいファイルが作成されるのを待ってから、何らかの方法でファイルに反応します。ここでの競合状態は興味深いものです。確かに、ここでの高レベルの要件は、イベントを取得してから、実際に(少なくとも)ファイルの読み取りロックを取得することです。大きなファイルや単に大量のファイルを作成する場合、これには、新しく作成されたファイルのロックを定期的に取得しようとするワーカースレッドのプール全体が必要になる可能性があり、成功すると実際に作業を行います。しかし、NTが認識していると確信しているように、これは最終的にはポーリングアプローチであり、スケーラビリティとポーリングはうまく調和する2つの言葉ではないため、スケーリングするには慎重に行う必要があります。

0
Stefan

それで、私は同じ問題を抱えていて、次の解決策が私のために働いていました。以前の失敗した試行-各ファイルの「lastModifiedTime」統計を監視しようとしましたが、大きなファイルのサイズの増加がしばらく一時停止する可能性があることに気付きました(サイズは継続的に変化しません)

基本的な考え方-すべてのイベントに対して、次の形式の名前のトリガーファイルを(一時ディレクトリに)作成します-

OriginalFileName_lastModifiedTime_numberOfTries

このファイルは空であり、すべてのプレイは名前だけにあります。元のファイルは、「最終変更時刻」の統計情報を変更せずに、特定の期間の間隔を通過した後にのみ考慮されます。 (注-これはファイル統計であるため、オーバーヘッドはありません-> O(1))

[〜#〜] note [〜#〜]-このトリガーファイルは別のサービスによって処理されます(たとえば 'FileTrigger ')。

利点-

  1. スリープしないか、システムを保持するのを待ちます。
  2. ファイルウォッチャーを解放して、他のイベントを監視します

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のコード(間隔は5分と言うcronジョブ)-

 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)
    }    
}
0
Varun Chaudhary

アップロードされたファイルを転送するためにファイルシステムウォッチャーを実装したときも、同様の状況に対処する必要がありました。この問題を解決するために実装したソリューションは、次のとおりです。

1-まず、未処理のファイルのマップを維持します(ファイルがまだコピーされている限り、ファイルシステムはModify_Eventを生成するため、フラグがfalseの場合は無視できます)。

2- fileProcessorで、リストからファイルを取得し、ファイルシステムによってロックされているかどうかを確認します。ロックされている場合は、例外が発生します。この例外をキャッチし、スレッドを待機状態(つまり、10秒)にしてから再試行します。ロックが解除されるまで再び。ファイルを処理した後、フラグをtrueに変更するか、マップから削除することができます。

同じファイルの多くのバージョンが待機タイムスロット中に転送される場合、このソリューションは効率的ではありません。

乾杯、ラムジ

0
Ramcis

書き込みが完了した後、ファイルをどれだけ緊急に移動する必要があるかに応じて、安定した最終変更タイムスタンプを確認し、静止しているファイルのみを移動することもできます。安定するために必要な時間は実装に依存する可能性がありますが、最後に変更されたタイムスタンプが15秒間変更されていないものは、移動するのに十分安定している必要があると思います。

0
Eric B.