Ensure we cannot accidentally open a replay file twice

E.g. from the Replay Viewer while it is still being recovered.
Because that would potentially corrupt it.
This commit is contained in:
Jonas Herzig
2021-11-28 15:38:51 +01:00
parent 31817bcf11
commit bd0dd10942
3 changed files with 269 additions and 6 deletions

View File

@@ -0,0 +1,202 @@
package com.replaymod.core.files;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.data.ModInfo;
import com.replaymod.replaystudio.data.ReplayAssetEntry;
import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.lib.guava.base.Optional;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.protocol.PacketTypeRegistry;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
public class DelegatingReplayFile implements ReplayFile {
private final ReplayFile delegate;
public DelegatingReplayFile(ReplayFile delegate) {
this.delegate = delegate;
}
@Override
public Optional<InputStream> get(String entry) throws IOException {
return this.delegate.get(entry);
}
@Override
public Optional<InputStream> getCache(String entry) throws IOException {
return this.delegate.getCache(entry);
}
@Override
public Map<String, InputStream> getAll(Pattern pattern) throws IOException {
return this.delegate.getAll(pattern);
}
@Override
public OutputStream write(String entry) throws IOException {
return this.delegate.write(entry);
}
@Override
public OutputStream writeCache(String entry) throws IOException {
return this.delegate.writeCache(entry);
}
@Override
public void remove(String entry) throws IOException {
this.delegate.remove(entry);
}
@Override
public void removeCache(String entry) throws IOException {
this.delegate.removeCache(entry);
}
@Override
public void save() throws IOException {
this.delegate.save();
}
@Override
public void saveTo(File target) throws IOException {
this.delegate.saveTo(target);
}
@Override
public ReplayMetaData getMetaData() throws IOException {
return this.delegate.getMetaData();
}
@Override
public void writeMetaData(PacketTypeRegistry registry, ReplayMetaData metaData) throws IOException {
this.delegate.writeMetaData(registry, metaData);
}
@Override
public ReplayInputStream getPacketData(PacketTypeRegistry registry) throws IOException {
return this.delegate.getPacketData(registry);
}
@Override
public ReplayOutputStream writePacketData() throws IOException {
return this.delegate.writePacketData();
}
@Override
public Map<Integer, String> getResourcePackIndex() throws IOException {
return this.delegate.getResourcePackIndex();
}
@Override
public void writeResourcePackIndex(Map<Integer, String> index) throws IOException {
this.delegate.writeResourcePackIndex(index);
}
@Override
public Optional<InputStream> getResourcePack(String hash) throws IOException {
return this.delegate.getResourcePack(hash);
}
@Override
public OutputStream writeResourcePack(String hash) throws IOException {
return this.delegate.writeResourcePack(hash);
}
@Override
public Map<String, Timeline> getTimelines(PathingRegistry pathingRegistry) throws IOException {
return this.delegate.getTimelines(pathingRegistry);
}
@Override
public void writeTimelines(PathingRegistry pathingRegistry, Map<String, Timeline> timelines) throws IOException {
this.delegate.writeTimelines(pathingRegistry, timelines);
}
@Override
public Optional<BufferedImage> getThumb() throws IOException {
return this.delegate.getThumb();
}
@Override
public void writeThumb(BufferedImage image) throws IOException {
this.delegate.writeThumb(image);
}
@Override
public Optional<InputStream> getThumbBytes() throws IOException {
return this.delegate.getThumbBytes();
}
@Override
public void writeThumbBytes(byte[] image) throws IOException {
this.delegate.writeThumbBytes(image);
}
@Override
public Optional<Set<UUID>> getInvisiblePlayers() throws IOException {
return this.delegate.getInvisiblePlayers();
}
@Override
public void writeInvisiblePlayers(Set<UUID> uuids) throws IOException {
this.delegate.writeInvisiblePlayers(uuids);
}
@Override
public Optional<Set<Marker>> getMarkers() throws IOException {
return this.delegate.getMarkers();
}
@Override
public void writeMarkers(Set<Marker> markers) throws IOException {
this.delegate.writeMarkers(markers);
}
@Override
public Collection<ReplayAssetEntry> getAssets() throws IOException {
return this.delegate.getAssets();
}
@Override
public Optional<InputStream> getAsset(UUID uuid) throws IOException {
return this.delegate.getAsset(uuid);
}
@Override
public OutputStream writeAsset(ReplayAssetEntry asset) throws IOException {
return this.delegate.writeAsset(asset);
}
@Override
public void removeAsset(UUID uuid) throws IOException {
this.delegate.removeAsset(uuid);
}
@Override
public Collection<ModInfo> getModInfo() throws IOException {
return this.delegate.getModInfo();
}
@Override
public void writeModInfo(Collection<ModInfo> modInfo) throws IOException {
this.delegate.writeModInfo(modInfo);
}
@Override
public void close() throws IOException {
this.delegate.close();
}
}

View File

@@ -0,0 +1,23 @@
package com.replaymod.core.files;
import com.replaymod.replaystudio.replay.ReplayFile;
import java.io.IOException;
public class ManagedReplayFile extends DelegatingReplayFile {
private Runnable onClose;
public ManagedReplayFile(ReplayFile delegate, Runnable onClose) {
super(delegate);
this.onClose = onClose;
}
@Override
public void close() throws IOException {
super.close();
onClose.run();
onClose = () -> {};
}
}

View File

@@ -17,9 +17,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ReplayFilesService {
private final ReplayFoldersService folders;
private final Set<Path> lockedPaths = Collections.newSetFromMap(new ConcurrentHashMap<>());
public ReplayFilesService(ReplayFoldersService folders) {
this.folders = folders;
@@ -30,12 +35,39 @@ public class ReplayFilesService {
}
public ReplayFile open(Path input, Path output) throws IOException {
return new ZipReplayFile(
new ReplayStudio(),
input != null ? input.toFile() : null,
output.toFile(),
folders.getCachePathForReplay(output).toFile()
);
Path realInput = input != null ? input.toAbsolutePath().normalize() : null;
Path realOutput = output.toAbsolutePath().normalize();
if (realInput != null && !lockedPaths.add(realInput)) {
throw new FileLockedException(realInput);
}
if (!Objects.equals(realInput, realOutput) && !lockedPaths.add(realOutput)) {
if (realInput != null) {
lockedPaths.remove(realInput);
}
throw new FileLockedException(realOutput);
}
Runnable onClose = () -> {
if (realInput != null) {
lockedPaths.remove(realInput);
}
lockedPaths.remove(realOutput);
};
ReplayFile replayFile;
try {
replayFile = new ZipReplayFile(
new ReplayStudio(),
realInput != null ? realInput.toFile() : null,
realOutput.toFile(),
folders.getCachePathForReplay(realOutput).toFile()
);
} catch (IOException e) {
onClose.run();
throw e;
}
return new ManagedReplayFile(replayFile, onClose);
}
public void initialScan(ReplayMod core) {
@@ -151,4 +183,10 @@ public class ReplayFilesService {
e.printStackTrace();
}
}
public static class FileLockedException extends IOException {
public FileLockedException(Path path) {
super(path.toString());
}
}
}