package org.jabref.logic.exporter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;
import java.util.Set;
import org.jabref.logic.util.io.FileBasedLock;
import org.jabref.logic.util.io.FileUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Class used to handle safe storage to disk.
* <p>
* Usage: create a SaveSession giving the file to save to, the encoding, and whether to make a backup. The SaveSession
* will provide a Writer to store to, which actually goes to a temporary file. The Writer keeps track of whether all
* characters could be saved, and if not, which characters were not encodable.
* <p>
* After saving is finished, the client should close the Writer. If the save should be put into effect, call commit(),
* otherwise call cancel(). When canceling, the temporary file is simply deleted and the target file remains unchanged.
* When committing, the temporary file is copied to the target file after making a backup if requested and if the target
* file already existed, and finally the temporary file is deleted.
* <p>
* If committing fails, the temporary file will not be deleted.
*/
public class FileSaveSession extends SaveSession {
private static final Log LOGGER = LogFactory.getLog(FileSaveSession.class);
// Filenames.
private static final String BACKUP_EXTENSION = ".bak";
private static final String TEMP_PREFIX = "jabref";
private static final String TEMP_SUFFIX = "save.bib";
private final Path temporaryFile;
public FileSaveSession(Charset encoding, boolean backup) throws SaveException {
this(encoding, backup, createTemporaryFile());
}
public FileSaveSession(Charset encoding, boolean backup, Path temporaryFile) throws SaveException {
super(encoding, backup, getWriterForFile(encoding, temporaryFile));
this.temporaryFile = temporaryFile;
}
private static VerifyingWriter getWriterForFile(Charset encoding, Path file) throws SaveException {
try {
return new VerifyingWriter(Files.newOutputStream(file), encoding);
} catch (IOException e) {
throw new SaveException(e);
}
}
private static Path createTemporaryFile() throws SaveException {
try {
return Files.createTempFile(FileSaveSession.TEMP_PREFIX, FileSaveSession.TEMP_SUFFIX);
} catch (IOException e) {
throw new SaveException(e);
}
}
@Override
public void commit(Path file) throws SaveException {
if (file == null) {
return;
}
if (backup && Files.exists(file)) {
Path backupFile = FileUtil.addExtension(file, BACKUP_EXTENSION);
FileUtil.copyFile(file, backupFile, true);
}
try {
// Always use a lock file
try {
if (FileBasedLock.createLockFile(file)) {
// Oops, the lock file already existed. Try to wait it out:
if (!FileBasedLock.waitForFileLock(file)) {
throw SaveException.FILE_LOCKED;
}
}
} catch (IOException ex) {
LOGGER.error("Error when creating lock file.", ex);
}
// Try to save file permissions to restore them later (by default: 664)
Set<PosixFilePermission> oldFilePermissions = EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.OTHERS_READ);
if (FileUtil.isPosixCompilant && Files.exists(file)) {
try {
oldFilePermissions = Files.getPosixFilePermissions(file);
} catch (IOException exception) {
LOGGER.warn("Error getting file permissions.", exception);
}
}
FileUtil.copyFile(temporaryFile, file, true);
// Restore file permissions
if (FileUtil.isPosixCompilant) {
try {
Files.setPosixFilePermissions(file, oldFilePermissions);
} catch (IOException exception) {
throw new SaveException(exception);
}
}
} finally {
FileBasedLock.deleteLockFile(file);
}
try {
Files.deleteIfExists(temporaryFile);
} catch (IOException e) {
LOGGER.warn("Cannot delete temporary file", e);
}
}
@Override
public void cancel() {
try {
Files.deleteIfExists(temporaryFile);
} catch (IOException e) {
LOGGER.warn("Cannot delete temporary file", e);
}
}
}