package com.github.ruediste1.btrbck;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.joda.time.Interval;
import org.joda.time.Period;
import org.joda.time.chrono.ISOChronology;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.ruediste1.btrbck.dom.ApplicationStreamRepository;
import com.github.ruediste1.btrbck.dom.Retention;
import com.github.ruediste1.btrbck.dom.Snapshot;
import com.github.ruediste1.btrbck.dom.Stream;
import com.github.ruediste1.btrbck.dom.StreamRepository;
import com.github.ruediste1.btrbck.dom.VersionHistory;
/**
* Provides operations on {@link Stream}s
*
*/
@Singleton
public class StreamService {
Logger log = LoggerFactory.getLogger(StreamService.class);
@Inject
BtrfsService btrfsService;
@Inject
StreamRepositoryService streamRepositoryService;
@Inject
JAXBContext ctx;
public Stream readStream(StreamRepository repository, String name) {
Stream result = tryReadStream(repository, name);
if (result == null) {
throw new DisplayException("Cannot read stream " + name
+ " in repository "
+ repository.rootDirectory.toAbsolutePath());
}
return result;
}
public Stream tryReadStream(StreamRepository repository, String name) {
Stream s = new Stream();
s.name = name;
s.streamRepository = repository;
if (!Files.isDirectory(s.getStreamMetaDirectory())) {
return null;
}
Stream readStream;
// read config file
try {
readStream = (Stream) ctx.createUnmarshaller().unmarshal(
s.getStreamConfigFile().toFile());
}
catch (JAXBException e) {
throw new RuntimeException(
"Error while reading stream config file", e);
}
readStream.name = name;
readStream.streamRepository = repository;
// read id
try {
readStream.id = UUID.fromString(new String(Files
.readAllBytes(readStream.getStreamUuidFile()), "UTF-8"));
}
catch (IOException e) {
throw new RuntimeException("Error while reading stream id file", e);
}
// read version history
try {
File historyFile = s.getVersionHistoryFile().toFile();
log.debug("reading version history from " + historyFile);
readStream.versionHistory = (VersionHistory) ctx
.createUnmarshaller().unmarshal(historyFile);
}
catch (JAXBException e) {
throw new RuntimeException("Error while reading version history", e);
}
log.debug("read stream " + readStream + ", versionHistory: "
+ readStream.versionHistory);
return readStream;
}
public Stream createStream(StreamRepository streamRepository, String name)
throws IOException {
if (tryReadStream(streamRepository, name) != null) {
throw new DisplayException("Stream " + name + " already exists");
}
Stream stream = new Stream();
stream.streamRepository = streamRepository;
stream.name = name;
Files.createDirectory(stream.getStreamMetaDirectory());
Files.createDirectory(stream.getSnapshotsDir());
Files.createDirectory(stream.getReceiveTempDir());
stream.id = UUID.randomUUID();
Files.write(stream.getStreamUuidFile(),
stream.id.toString().getBytes("UTF-8"));
if (stream.streamRepository instanceof ApplicationStreamRepository) {
Path workingDirectory = ((ApplicationStreamRepository) stream.streamRepository)
.getWorkingDirectory(stream);
btrfsService.createSubVolume(workingDirectory);
}
// write stream config
InputStream in = getClass().getClassLoader().getResourceAsStream(
"stream.template.xml");
Files.copy(in, stream.getStreamConfigFile());
// initialize version history
stream.versionHistory = new VersionHistory();
writeVersionHistory(stream);
return stream;
}
public void writeVersionHistory(Stream stream) {
try {
ctx.createMarshaller().marshal(stream.versionHistory,
stream.getVersionHistoryFile().toFile());
}
catch (JAXBException e) {
throw new RuntimeException("Error while writing stream", e);
}
}
public Set<String> getStreamNames(StreamRepository repository) {
return Util.getDirectoryNames(repository.getBaseDirectory());
}
public void deleteStream(Path repoLocation, String name) {
deleteStream(readStream(
streamRepositoryService.readRepository(repoLocation), name));
}
public void deleteStream(StreamRepository repo, String name) {
deleteStream(readStream(repo, name));
}
public void deleteStream(Stream stream) {
if (stream.streamRepository instanceof ApplicationStreamRepository) {
Path workingDirectory = ((ApplicationStreamRepository) stream.streamRepository)
.getWorkingDirectory(stream);
btrfsService.deleteSubVolume(workingDirectory);
}
for (Snapshot snapshot : getSnapshots(stream).values()) {
deleteSnapshot(snapshot);
}
clearReceiveTempDir(stream);
Util.removeRecursive(stream.getStreamMetaDirectory(), true);
}
public void deleteStreams(StreamRepository repository) {
Set<String> streamNames = getStreamNames(repository);
for (String name : streamNames) {
Stream s = new Stream();
s.name = name;
s.streamRepository = repository;
deleteStream(s);
}
}
/**
* Return the snapshots in the stream, sorted by their number
*/
public TreeMap<Integer, Snapshot> getSnapshots(Stream stream) {
TreeMap<Integer, Snapshot> result = new TreeMap<>();
for (String name : Util.getDirectoryNames(stream.getSnapshotsDir())) {
Snapshot snapshot = Snapshot.parse(stream, name);
// read sender id if exists
if (Files.exists(stream.getSnapshotSenderIdFile(name))) {
try {
snapshot.senderStreamId = UUID.fromString(new String(
Files.readAllBytes(stream
.getSnapshotSenderIdFile(name)), "UTF-8"));
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
result.put(snapshot.nr, snapshot);
}
return result;
}
public Snapshot takeSnapshot(Stream stream) {
if (!(stream.streamRepository instanceof ApplicationStreamRepository)) {
throw new DisplayException(
"Cannot take a snapshot of the working directory of stream "
+ stream.name
+ " in non-application stream repository "
+ stream.streamRepository.rootDirectory
.toAbsolutePath());
}
ApplicationStreamRepository repo = (ApplicationStreamRepository) stream.streamRepository;
Snapshot snapshot = new Snapshot();
snapshot.stream = stream;
snapshot.date = new DateTime();
snapshot.nr = stream.versionHistory.getVersionCount();
stream.versionHistory.addVersion(stream.id);
writeVersionHistory(stream);
btrfsService.takeSnapshot(repo.getWorkingDirectory(stream),
snapshot.getSnapshotDir(), true);
return snapshot;
}
public void restoreLatestSnapshot(Stream stream) {
TreeMap<Integer, Snapshot> snapshots = getSnapshots(stream);
if (snapshots.isEmpty()) {
throw new DisplayException(
"Cannot restore latest snapshot. Stream " + stream.name
+ " does not contain any snapshots");
}
restoreSnapshot(stream, Collections.max(snapshots.keySet()));
}
public void restoreSnapshot(Stream stream, int snapshotNr) {
TreeMap<Integer, Snapshot> snapshots = getSnapshots(stream);
Snapshot snapshot = snapshots.get(snapshotNr);
if (snapshot == null) {
throw new DisplayException("Cannot restore snapshot. Stream "
+ stream.name + " does not contain snapshot number "
+ snapshotNr);
}
restoreSnapshot(snapshot);
}
/**
* Restore a snapshot.
*
* The following list outlines the steps taken:
* <ol>
* <li>delete working directory</li>
* <li>update version file</li>
* <li>restore working directory</li>
* </ol>
*
* If the process is aborted at any stage (power loss), the command can
* simply be executed again.
*/
public void restoreSnapshot(Snapshot snapshot) {
Stream stream = snapshot.stream;
ApplicationStreamRepository repo = (ApplicationStreamRepository) stream.streamRepository;
btrfsService.deleteSubVolume(repo.getWorkingDirectory(stream));
stream.versionHistory.addRestore(stream.id, snapshot.nr);
writeVersionHistory(stream);
btrfsService.takeSnapshot(snapshot.getSnapshotDir(),
repo.getWorkingDirectory(stream), false);
}
public void deleteSnapshot(Snapshot snapshot) {
try {
Files.deleteIfExists(snapshot.stream
.getSnapshotSenderIdFile(snapshot.getSnapshotName()));
}
catch (IOException e) {
throw new RuntimeException("error while deleting senderId file", e);
}
btrfsService.deleteSubVolume(snapshot.getSnapshotDir());
}
public void clearReceiveTempDir(Stream stream) {
for (String name : Util.getDirectoryNames(stream.getReceiveTempDir())) {
btrfsService.deleteSubVolume(stream.getReceiveTempDir().resolve(
name));
}
}
/**
* Determine if a new snapshot is required, given the current time
*/
public void takeSnapshotIfRequired(Stream stream, Instant now) {
if (isSnapshotRequired(now, stream.snapshotInterval,
getSnapshots(stream).values())) {
takeSnapshot(stream);
}
}
boolean isSnapshotRequired(Instant now, Period snapshotInterval,
Collection<Snapshot> snapshots) {
if (snapshotInterval == null) {
return false;
}
DateTime latest = null;
for (Snapshot s : snapshots) {
if (latest == null || latest.isBefore(s.date)) {
latest = s.date;
}
}
log.debug("Latest snapshot date: " + latest + " interval: "
+ snapshotInterval + " now: " + now);
boolean snapshotRequired = true;
if (latest != null) {
if (latest.plus(snapshotInterval).isAfter(now)) {
snapshotRequired = false;
}
}
return snapshotRequired;
}
public void pruneSnapshots(Stream stream) {
if (stream.initialRetentionPeriod == null
&& stream.retentions.isEmpty()) {
// no retentions, do not prune
return;
}
DateTime now = new DateTime(ISOChronology.getInstanceUTC());
TreeMap<DateTime, Boolean> keepSnapshot = new TreeMap<>();
HashMap<DateTime, Snapshot> snapshotMap = new HashMap<>();
Interval initialRetentionInterval = stream
.getInitialRetentionInterval(now);
// fill maps
for (Snapshot s : getSnapshots(stream).values()) {
keepSnapshot.put(s.date, s.date.isAfter(now)
|| initialRetentionInterval.contains(s.date));
snapshotMap.put(s.date, s);
}
// process retentions
for (Retention r : stream.retentions) {
for (DateTime time : r.retentionTimes(now)) {
DateTime key = keepSnapshot.ceilingKey(time);
if (key != null) {
keepSnapshot.put(key, true);
}
}
}
// delete streams which are not to be retained
for (Entry<DateTime, Boolean> entry : keepSnapshot.entrySet()) {
if (!entry.getValue()) {
deleteSnapshot(snapshotMap.get(entry.getKey()));
}
}
}
}