Verified Owner
Top Description Inners Fields Constructors Methods
org.jabref.logic.exporter

public Class AtomicFileOutputStream

extends FilterOutputStream
Class Inheritance
Annotations
@NullMarked
Imports
java.io.FilterOutputStream, .IOException, .InputStream, .InterruptedIOException, .OutputStream, java.nio.channels.Channels, .FileChannel, java.nio.file.AtomicMoveNotSupportedException, .FileSystemException, .Files, .Path, .StandardCopyOption, .StandardOpenOption, java.nio.file.attribute.PosixFilePermission, java.util.EnumSet, .LinkedHashMap, .List, .Map, .Optional, .Set, org.jabref.logic.os.OS, org.jabref.logic.util.BackupFileType, org.jabref.logic.util.io.FileSnapshot, .FileUtil, org.jspecify.annotations.NullMarked, .Nullable, org.slf4j.Logger, .LoggerFactory

A file output stream that is similar to the standard java.io.FileOutputStream, except that all writes are first redirected to a temporary file. When the stream is closed, the temporary file (atomically) replaces the target file.

In detail, the strategy is to:

  1. Write to a temporary file (with .tmp suffix) in the same directory as the destination file.
  2. Create a backup (with .sav suffix) of the original file (if it exists) in the same directory.
  3. Atomically move the temporary file to the correct place, overwriting any file that already exists at that location. On Linux and macOS, files with hard links are overwritten in place to preserve their inode. An in-place overwrite is also used when the file system does not support atomic moves. Afterwards, the group, DOS attributes, ACL and user-defined extended attributes of the original file are applied to the new file on a best-effort basis (the owner cannot be restored without elevated privileges).
  4. Delete the backup file (if configured to do so).

If all goes well, no temporary or backup files will remain on disk after closing the stream.

Errors are handled as follows:

  1. If anything goes wrong while writing to the temporary file, the temporary file will be deleted (leaving the original file untouched).
  2. If anything goes wrong while copying the temporary file to the target file, the backup of the original file is kept.
  3. If the target file was modified by another process between opening this stream and committing it (e.g. a second JabRef instance saving the same library), the commit is aborted with a FileChangedException, leaving the other process's version of the file untouched.

Implementation inspired by code from Marty Lamb and Apache.

Nested and Inner Type Summary

Modifier and TypeClass and Description
pack-priv static interface
pack-priv static interface

Field Summary

Modifier and TypeField and Description
private final Path
backupFile

A backup of the target file (if it exists), created when the stream is closed

private final AtomicFileOutputStream.FileCopyOperation
private @Nullable FileSnapshot
committedTargetFileState

State of the target file right after a successful commit; null before the commit, after an aborted or failed one, and when the attributes could not be read.

private boolean
private final @Nullable FileSnapshot
expectedTargetFileState

The state the target file must still be in when this stream commits: an inherited baseline (see the AtomicFileOutputStream(Path, boolean, FileSnapshot) constructor), or the state at stream creation.

private final AtomicFileOutputStream.FileMoveOperation
private final boolean
private static final Logger
private static final int
MOVE_ATTEMPTS

Number of attempts to move the temporary file onto the target file.

private static final long
MOVE_RETRY_INITIAL_DELAY_MILLIS

Delay before the second move attempt; doubled after each further failed attempt.

private static final String
private final Path
targetFile

The file we want to create/replace.

private static final String
private static final String
private final Path
temporaryFile

The uniquely named file to which writes are redirected to.

private final @Nullable FileChannel
temporaryFileChannel

Null if the stream was constructed from an injected OutputStream (tests), because syncing is not possible then.

Inherited from java.io.FilterOutputStream:
out

Constructor Summary

AccessConstructor and Description
public
AtomicFileOutputStream(Path
the path of the file to write to or replace
path
,
boolean
whether to keep the backup file (.sav) after a successful write process
keepBackup
)

Creates a new output stream to write to or replace the file at the specified path.

public
AtomicFileOutputStream(Path
the path of the file to write to or replace
path
,
boolean
whether to keep the backup file (.sav) after a successful write process
keepBackup
,
@Nullable FileSnapshot
the state the target file is expected to (still) be in when this stream commits — a snapshot from an earlier point of the same logical operation, so that a concurrent write landing before this stream was even opened is still detected; null to verify against the state at stream creation
expectedState
)

Creates a new output stream to write to or replace the file at the specified path, verifying against an inherited baseline instead of the file's state at stream creation.

private
AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, boolean keepBackup, @Nullable FileSnapshot expectedState, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)

The temporary file is opened as a FileChannel, because the channel is needed for FileChannel#force(boolean).

private
AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, FileChannel temporaryFileChannel, boolean keepBackup, @Nullable FileSnapshot expectedState, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)

public
AtomicFileOutputStream(Path
the path of the file to write to or replace
path
)

Creates a new output stream to write to or replace the file at the specified path.

pack-priv
AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, boolean keepBackup)

Required for proper testing

pack-priv
AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, boolean keepBackup, AtomicFileOutputStream.FileMoveOperation fileMoveOperation)

Required for proper testing

pack-priv
AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, boolean keepBackup, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)

Required for proper testing

private
AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, @Nullable FileChannel temporaryFileChannel, boolean keepBackup, @Nullable FileSnapshot expectedState, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)

Method Summary

Modifier and TypeMethod and Description
public void
abort()

Closes the write process to the temporary file but does not commit to the target file.

private static void
applyAttributes(Path file, Map<String, Object> attributes)

Applies the attributes read by readPreservableAttributes(Path, Path) in their order, skipping (and logging) each one the OS refuses, e.g. a group the user is not a member of.

private void
public void
close()

Overrides java.io.FilterOutputStream.close.

Implements java.io.Closeable.close.

perform the final operations to move the temporary file to its final destination

private void
private static void
private boolean
private static Path
private void
ensureTargetFileUnchanged()

Best-effort lost-update guard: detects whether another process modified the target file after the expected baseline state was captured.

private void
public void
flush()

Overrides java.io.FilterOutputStream.flush.

Implements java.io.Flushable.flush.

Flushes this output stream and forces any buffered output bytes to be written out to the stream.

private static void
public Path
getBackup()

Returns the path of the backup copy of the original file (may not exist)

public @Nullable FileSnapshot

Returns:

the committed state, or null when the stream did not commit (yet) or the attributes could not be read
getCommittedTargetFileState
()

Returns the state of the target file as written by this stream, captured immediately after the successful commit.

private static Path
private void
private static void
moveAtomically(Path source, Path target)

private void
moveBackupFileIntoPlace(Path temporaryBackupFile)

private void
moveTemporaryFileToTargetFile(boolean backupCreated)

Moves the temporary file onto the target file, replacing it.

private void
private void
overwriteTargetFile(boolean backupCreated)

private static Map<String, Object>
readPreservableAttributes(Path file, Path
the file that is going to replace file; only DOS flags differing from its flags are included, because writing an unchanged false on Linux adds a DOSATTRIB xattr to every saved file
replacement
)

Reads everything a move cannot preserve (group, DOS attributes, ACL, user-defined extended attributes) from file, keyed by attribute name for Files#setAttribute, in the order they have to be applied: the ACL after the group and extended attributes because it may revoke the access to write them, the DOS flags after the ACL because writing it sets the archive bit, and read-only last of all.

public void
write(byte[] b, int off, int len)

Overrides java.io.FilterOutputStream.write.

Overridden because of cleanup actions in case of an error

public void
write(int b)

Overrides java.io.FilterOutputStream.write.

Writes the specified byte to this output stream.

Inherited from java.io.FilterOutputStream:
write

Field Detail

backupFileback to summary
private final Path backupFile

A backup of the target file (if it exists), created when the stream is closed

backupFileCopyOperationback to summary
private final AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation
committedTargetFileStateback to summary
private @Nullable FileSnapshot committedTargetFileState

State of the target file right after a successful commit; null before the commit, after an aborted or failed one, and when the attributes could not be read.

errorDuringWriteback to summary
private boolean errorDuringWrite
expectedTargetFileStateback to summary
private final @Nullable FileSnapshot expectedTargetFileState

The state the target file must still be in when this stream commits: an inherited baseline (see the AtomicFileOutputStream(Path, boolean, FileSnapshot) constructor), or the state at stream creation. null if the attributes could not be read, in which case concurrent-change detection is disabled for this write.

fileMoveOperationback to summary
private final AtomicFileOutputStream.FileMoveOperation fileMoveOperation
keepBackupback to summary
private final boolean keepBackup
LOGGERback to summary
private static final Logger LOGGER
MOVE_ATTEMPTSback to summary
private static final int MOVE_ATTEMPTS

Number of attempts to move the temporary file onto the target file. See moveTemporaryFileToTargetFile().

MOVE_RETRY_INITIAL_DELAY_MILLISback to summary
private static final long MOVE_RETRY_INITIAL_DELAY_MILLIS

Delay before the second move attempt; doubled after each further failed attempt.

SAVE_EXTENSIONback to summary
private static final String SAVE_EXTENSION
targetFileback to summary
private final Path targetFile

The file we want to create/replace.

TEMPORARY_EXTENSIONback to summary
private static final String TEMPORARY_EXTENSION
TEMPORARY_FILE_PREFIXback to summary
private static final String TEMPORARY_FILE_PREFIX
temporaryFileback to summary
private final Path temporaryFile

The uniquely named file to which writes are redirected to.

temporaryFileChannelback to summary
private final @Nullable FileChannel temporaryFileChannel

Null if the stream was constructed from an injected OutputStream (tests), because syncing is not possible then.

Constructor Detail

AtomicFileOutputStreamback to summary
public AtomicFileOutputStream(Path path, boolean keepBackup) throws IOException

Creates a new output stream to write to or replace the file at the specified path.

Parameters
path:Path

the path of the file to write to or replace

keepBackup:boolean

whether to keep the backup file (.sav) after a successful write process

AtomicFileOutputStreamback to summary
public AtomicFileOutputStream(Path path, boolean keepBackup, @Nullable FileSnapshot expectedState) throws IOException

Creates a new output stream to write to or replace the file at the specified path, verifying against an inherited baseline instead of the file's state at stream creation.

Parameters
path:Path

the path of the file to write to or replace

keepBackup:boolean

whether to keep the backup file (.sav) after a successful write process

expectedState:@Nullable FileSnapshot

the state the target file is expected to (still) be in when this stream commits — a snapshot from an earlier point of the same logical operation, so that a concurrent write landing before this stream was even opened is still detected; null to verify against the state at stream creation

AtomicFileOutputStreamback to summary
private AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, boolean keepBackup, @Nullable FileSnapshot expectedState, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation) throws IOException

The temporary file is opened as a FileChannel, because the channel is needed for FileChannel#force(boolean). Files.newOutputStream(...) returns a sun.nio.ch.ChannelOutputStream, which does not offer it.

AtomicFileOutputStreamback to summary
private AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, FileChannel temporaryFileChannel, boolean keepBackup, @Nullable FileSnapshot expectedState, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)
AtomicFileOutputStreamback to summary
public AtomicFileOutputStream(Path path) throws IOException

Creates a new output stream to write to or replace the file at the specified path. The backup file (.sav) is deleted when write was successful.

Parameters
path:Path

the path of the file to write to or replace

AtomicFileOutputStreamback to summary
pack-priv AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, boolean keepBackup)

Required for proper testing

AtomicFileOutputStreamback to summary
pack-priv AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, boolean keepBackup, AtomicFileOutputStream.FileMoveOperation fileMoveOperation)

Required for proper testing

AtomicFileOutputStreamback to summary
pack-priv AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, boolean keepBackup, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)

Required for proper testing

AtomicFileOutputStreamback to summary
private AtomicFileOutputStream(Path path, Path pathOfTemporaryFile, OutputStream temporaryFileOutputStream, @Nullable FileChannel temporaryFileChannel, boolean keepBackup, @Nullable FileSnapshot expectedState, AtomicFileOutputStream.FileMoveOperation fileMoveOperation, AtomicFileOutputStream.FileCopyOperation backupFileCopyOperation)

Method Detail

abortback to summary
public void abort()

Closes the write process to the temporary file but does not commit to the target file.

applyAttributesback to summary
private static void applyAttributes(Path file, Map<String, Object> attributes)

Applies the attributes read by readPreservableAttributes(Path, Path) in their order, skipping (and logging) each one the OS refuses, e.g. a group the user is not a member of.

cleanupback to summary
private void cleanup()
closeback to summary
public void close() throws IOException

Overrides java.io.FilterOutputStream.close.

Implements java.io.Closeable.close.

perform the final operations to move the temporary file to its final destination

Annotations
@Override
Exceptions
IOException:

Doc from java.io.FilterOutputStream.close.

if an I/O error occurs.

copyFileToTargetback to summary
private void copyFileToTarget(Path source) throws IOException
copyReplacingExistingback to summary
private static void copyReplacingExisting(Path source, Path target) throws IOException
createBackupback to summary
private boolean createBackup()
createTemporaryFileback to summary
private static Path createTemporaryFile(Path targetFile) throws IOException
ensureTargetFileUnchangedback to summary
private void ensureTargetFileUnchanged() throws FileChangedException

Best-effort lost-update guard: detects whether another process modified the target file after the expected baseline state was captured. See FileSnapshot for the limits of the comparison; additionally, there is an unavoidable race between this check and the subsequent commit, so the check is repeated as late as possible. An unreadable current state does not abort the write (the commit itself will surface real I/O problems).

fallBackToInPlaceSaveback to summary
private void fallBackToInPlaceSave(IOException moveException) throws IOException
flushback to summary
public void flush() throws IOException

Overrides java.io.FilterOutputStream.flush.

Implements java.io.Flushable.flush.

Doc from java.io.FilterOutputStream.flush.

Flushes this output stream and forces any buffered output bytes to be written out to the stream.

Annotations
@Override
Exceptions
IOException:
if an I/O error occurs.
forceFileToDiskback to summary
private static void forceFileToDisk(Path file) throws IOException
getBackupback to summary
public Path getBackup()

Returns the path of the backup copy of the original file (may not exist)

getCommittedTargetFileStateback to summary
public @Nullable FileSnapshot getCommittedTargetFileState()

Returns the state of the target file as written by this stream, captured immediately after the successful commit. Callers spanning a longer logical operation (e.g. a save that may be retried with a different encoding after a user dialog) can pass it as the expected state of a follow-up stream, so that the whole operation is guarded against concurrent writes — including the time between the two streams.

Returns:@Nullable FileSnapshot

the committed state, or null when the stream did not commit (yet) or the attributes could not be read

getPathOfSaveBackupFileback to summary
private static Path getPathOfSaveBackupFile(Path targetFile)
markWriteAsFailedback to summary
private void markWriteAsFailed()
moveAtomicallyback to summary
private static void moveAtomically(Path source, Path target) throws IOException
moveBackupFileIntoPlaceback to summary
private void moveBackupFileIntoPlace(Path temporaryBackupFile) throws IOException
moveTemporaryFileToTargetFileback to summary
private void moveTemporaryFileToTargetFile(boolean backupCreated) throws IOException

Moves the temporary file onto the target file, replacing it.

On Windows, replacing a file requires DELETE access to it. That fails with a sharing violation (ERROR_SHARING_VIOLATION) while any other process holds a handle that was opened without FILE_SHARE_DELETE. Qt-based editors (for example TeXstudio, which re-reads .bib files while the user types), anti-virus scanners and the search indexer all open files that way, typically only for a few milliseconds. Retrying briefly therefore clears virtually all of these collisions.

See <#11916>.

moveTemporaryFileWithoutAtomicityback to summary
private void moveTemporaryFileWithoutAtomicity(IOException atomicMoveException) throws IOException
overwriteTargetFileback to summary
private void overwriteTargetFile(boolean backupCreated) throws IOException
readPreservableAttributesback to summary
private static Map<String, Object> readPreservableAttributes(Path file, Path replacement)

Reads everything a move cannot preserve (group, DOS attributes, ACL, user-defined extended attributes) from file, keyed by attribute name for Files#setAttribute, in the order they have to be applied: the ACL after the group and extended attributes because it may revoke the access to write them, the DOS flags after the ACL because writing it sets the archive bit, and read-only last of all. Each part is independent and best-effort: a mounted file system may lack support the default file system advertises. Ownership is not included, because restoring it needs elevated privileges. POSIX permissions are restored separately, so that they also apply to a freshly created target.

The string-based attribute API is used instead of the typed views: it never yields null and transfers each extended attribute completely or throws, so a partial transfer cannot go unnoticed.

Parameters
replacement:Path

the file that is going to replace file; only DOS flags differing from its flags are included, because writing an unchanged false on Linux adds a DOSATTRIB xattr to every saved file

writeback to summary
public void write(byte[] b, int off, int len) throws IOException

Overrides java.io.FilterOutputStream.write.

Overridden because of cleanup actions in case of an error

Annotations
@Override
Exceptions
IOException:

Doc from java.io.FilterOutputStream.write.

if an I/O error occurs.

writeback to summary
public void write(int b) throws IOException

Overrides java.io.FilterOutputStream.write.

Doc from java.io.FilterOutputStream.write.

Writes the specified byte to this output stream.

The write method of FilterOutputStream calls the write method of its underlying output stream, that is, it performs out.write(b).

Implements the abstract write method of OutputStream.

Annotations
@Override
Exceptions
IOException:
if an I/O error occurs.
org.jabref.logic.exporter back to summary

pack-priv Interface AtomicFileOutputStream.FileCopyOperation

Annotations
@FunctionalInterface

Method Summary

Modifier and TypeMethod and Description
public void
copy(Path source, Path target)

Method Detail

copyback to summary
public void copy(Path source, Path target) throws IOException
org.jabref.logic.exporter back to summary

pack-priv Interface AtomicFileOutputStream.FileMoveOperation

Annotations
@FunctionalInterface

Method Summary

Modifier and TypeMethod and Description
public void
move(Path source, Path target)

Method Detail

moveback to summary
public void move(Path source, Path target) throws IOException