package project;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Comparator;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.xml.stream.XMLStreamException;
import utils.lists.ArrayList;
import utils.lists.Arrays;
import utils.lists.Files;
import utils.lists.HashMap;
import utils.lists.HashSet;
import utils.lists.List;
import utils.lists.Map;
import utils.lists.Paths;
import utils.lists.Set;
import utils.streams.functions.ExDoubleConsumer;
import utils.streams.functions.IOFunction;
import utils.streams2.IntStream;
import utils.streams2.Stream;
import utils.streams2.Streams;
public class Snapshot {
public static class Content {
byte[] bytes;
Instant modified;
HashSet<Path> contained = new HashSet<>();
Content(Instant modified) {
this.modified = modified;
}
@Override
public String toString() {
return "[" +
(bytes != null ? "bytes=" + bytes.length + ", " : "") +
(modified != null ? "modified=" + modified + ", " : "") +
(contained != null ? "contained=" + contained.size() : "") +
"]";
}
}
private static final Path ROOT = Paths.get("a");
private HashMap<Path, Content> contents = new HashMap<>();
private Instant now;
private static final Comparator<Path> LONGEST_FIRST = Comparator.comparingInt(p -> -p.getNameCount());
public final IOFunction<Path, Path> BUNDLE_REMAPPER = this::remapper;
private List<IOFunction<Path, Path>> remappers = List.of(BUNDLE_REMAPPER);
private int progress;
private int progressTotal;
public static void main(String[] args) throws IOException {
Path ide = Paths.get("C:\\Evening-IDE\\projects\\IDE\\target\\ide.zip");
Instant instant = Instant.now();
Snapshot snapshot = new Snapshot(instant);
snapshot.read(ide);
Path touch = Paths.get("plugins", "touch");
snapshot.addFile(touch, new byte[0], instant);
snapshot.removeFile(touch);
Path ide2 = ide.resolveSibling("ide2.zip");
snapshot.write(ide2);
Path ide3 = ide.resolveSibling("ide3");
snapshot.write(ide3);
}
public Snapshot(Instant now) {
this.now = now;
}
public void clearPathRemappers() {
remappers = List.of();
}
public Snapshot addPathRemapper(IOFunction<Path, Path> mapper) {
remappers = remappers.add(mapper);
return this;
}
public Snapshot addPathRemapper(String path1, String path2) {
Path src = Paths.get(path1);
Path dst = Paths.get(path2);
addPathRemapper(p -> p.equals(src) ? dst : p);
return this;
}
public void addFile(Path path, byte[] bytes, Instant modified) throws IOException {
addFileImplementation(path, bytes, modified);
}
public void addFile(Path path, String data, Instant modified) throws IOException {
addFileImplementation(path, data.getBytes(StandardCharsets.UTF_8), modified);
}
public void replaceFile(Path path, byte[] bytes) throws IOException {
replaceFileImplementation(path, bytes);
}
public void replaceFile(Path path, String data) throws IOException {
replaceFileImplementation(path, data.getBytes(StandardCharsets.UTF_8));
}
public void copyFiles(String srcFolder, String dstFolder) throws IOException {
copyFilesImplementation(srcFolder, dstFolder);
}
public void addFolder(Path path, Instant modified) throws IOException {
addFileImplementation(path, null, modified);
}
public void removeFile(Path path) {
removeFileImplementation(path);
}
public List<Path> listFiles(Path path) {
return listFilesOrFoldersImplementation(path, true);
}
public List<Path> listFolders(Path path) {
return listFilesOrFoldersImplementation(path, false);
}
public List<Path> listAll(Path path) {
Content content = contents.get(ROOT.resolve(path));
if(content == null) {
return List.of();
}
return content.contained.toArrayList().sort().replaceAll(ROOT::relativize).toList();
}
public String getFileAsString(Path path) {
byte[] bytes = getFileImplementation(path);
if(bytes == null) {
return null;
}
return new String(bytes, StandardCharsets.UTF_8);
}
public Feature getFileAsFeature(Path path) throws IOException {
byte[] bytesFeatureXML = getFileImplementation(path.resolve("feature.xml"));
if(bytesFeatureXML == null) {
return null;
}
byte[] bytesFeatureProperties = getFileImplementation(path.resolve("feature.properties"));
try {
return Feature.fromXML(path, bytesFeatureXML, bytesFeatureProperties);
} catch(XMLStreamException e) {
throw new IOException(e);
}
}
public List<Path> listFilesRecursive(Path path) {
ArrayList<Path> result = new ArrayList<>();
for(Path folder : listFolders(path)) {
result.addAll(listFilesRecursive(folder));
}
for(Path folder : listFiles(path)) {
result.addAll(listFilesRecursive(folder));
result.add(folder);
}
return result.toList();
}
public Plugin getFileAsPlugin(Path path) throws IOException {
byte[] bytesManifestMf = getFileImplementation(path.resolve(JarFile.MANIFEST_NAME));
if(bytesManifestMf == null) {
return null;
}
return new Plugin(path, bytesManifestMf);
}
public Map<String, String> getFileAsProperties(Path path) throws IOException {
byte[] bytesProperties = getFileImplementation(path);
if(bytesProperties == null) {
return Map.of();
}
Properties properties = new Properties();
properties.load(new ByteArrayInputStream(bytesProperties));
Set<String> names = Set.from(properties.stringPropertyNames());
return names.stream().toMap(s -> s, s -> properties.getProperty(s)).toMap();
}
public boolean notFound(Path path) {
return containsImplementation(path) == false;
}
public boolean isFound(Path path) {
return containsImplementation(path);
}
public void write(Path path) throws IOException {
write(path, d -> {});
}
public <E extends Exception> void write(Path path, ExDoubleConsumer<E> monitor) throws IOException, E {
boolean isZIP = path.getFileName().toString().endsWith(".zip");
int level = isZIP ? 0 : 9;
Content content = contents.get(ROOT);
writeSnapshot(path, content, level, monitor);
monitor.accept(1);
}
public void read(Path start) throws IOException {
if(Files.isDirectory(start)) {
ArrayList<Path> list = Files.walk(start).toList();
for(int i = 0, n = list.size(); i < n; i++) {
Path entry = list.get(i);
if(Files.isRegularFile(entry)) {
byte[] bytes = Files.readAllBytes(entry);
Instant instant = Files.getLastModifiedTime(entry).toInstant();
String name = entry.getFileName().toString();
Path path = ROOT.resolve(relative(start, entry));
readFile(bytes, path, name, instant);
}
}
} else if(Files.isRegularFile(start)) {
byte[] bytes = Files.readAllBytes(start);
Instant instant = Files.getLastModifiedTime(start).toInstant();
String name = start.getFileName().toString();
readFile(bytes, ROOT, name, instant);
contents.get(ROOT).bytes = null;
}
}
public void clear() {
contents.clear();
}
private Path remapper(Path src) throws IOException {
if(src.getNameCount() == 2) {
if(src.startsWith(Paths.get("plugins"))) {
Content content = contents.get(ROOT.resolve(src).resolve(JarFile.MANIFEST_NAME));
if(content != null) {
Plugin plugin = new Plugin(src, content.bytes);
return src.resolveSibling(plugin.fileName());
}
} else if(src.startsWith(Paths.get("features"))) {
Content content = contents.get(ROOT.resolve(src).resolve("feature.xml"));
if(content != null) {
Content properties = contents.get(ROOT.resolve(src).resolve("feature.properties"));
try {
Feature feature =
Feature.fromXML(src, content.bytes, properties != null ? properties.bytes : null);
return src.resolveSibling(feature.fileName());
} catch(XMLStreamException e) {
throw new IOException(e);
}
}
}
}
return src;
}
private void copyFilesImplementation(String srcFolder, String dstFolder) throws IOException {
Path dstPath = Paths.get(dstFolder);
removeFileImplementation(dstPath);
Path srcPath = Paths.get(srcFolder);
List<Path> files = listFilesOrFoldersImplementation(srcPath, true);
for(Path filePath : files) {
Path targetPath = dstPath.resolve(srcPath.relativize(filePath));
byte[] targetBytes = getFileImplementation(filePath);
Instant targetInstant = getInstantImplementation(filePath);
addFileImplementation(targetPath, targetBytes, targetInstant);
}
}
private void addFileImplementation(Path path, byte[] bytes, Instant when) throws IOException {
Path root = ROOT.resolve(path);
String name = path.getFileName().toString();
readFile(bytes, root, name, when);
}
private void replaceFileImplementation(Path path, byte[] bytes) throws IOException {
Path root = ROOT.resolve(path);
String name = path.getFileName().toString();
Instant modified = contents.containsKey(root) ? contents.get(root).modified : now;
readFile(bytes, root, name, modified);
}
private byte[] getFileImplementation(Path path) {
Path root = ROOT.resolve(path);
Content content = contents.get(root);
if(content == null) {
return null;
}
return content.bytes;
}
private Instant getInstantImplementation(Path path) {
Path root = ROOT.resolve(path);
Content content = contents.get(root);
if(content == null) {
return null;
}
return content.modified;
}
private boolean containsImplementation(Path path) {
Path root = ROOT.resolve(path);
return contents.containsKey(root);
}
private void readFile(byte[] bytes, Path root, String name, Instant modified) throws IOException {
internalAddFile(root, bytes, modified);
if(name.endsWith(".jar") || name.endsWith(".zip")) {
readZip(bytes, root);
} else if(name.endsWith(".gz")) {
root = root.resolve(name.substring(0, name.length() - 3));
bytes = Streams.readFully(new GZIPInputStream(new ByteArrayInputStream(bytes)));
internalAddFile(root, bytes, modified);
}
}
private void readZip(byte[] bytes, Path root) throws IOException {
try(ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(bytes));) {
ZipEntry entry;
while((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if(name.contains("..")) {
continue;
}
if(!IntStream.from(name).allMatch(
i -> i >= 'a' &&
i <= 'z' ||
i >= 'A' &&
i <= 'Z' ||
i >= '0' &&
i <= '9' ||
i == '.' ||
i == '/' ||
i == '$' ||
i == ' ' ||
i == '_' ||
i == '(' ||
i == ')' ||
i == '-')) {
System.out.println("Skipped " + Arrays.toString(name.toCharArray()));
continue;
}
Path path = root.resolve(name);
Instant instant = entry.getLastModifiedTime().toInstant();
if(entry.isDirectory()) {
internalAddFile(path, null, instant);
} else {
bytes = Streams.readAllBytes(zip);
readFile(bytes, path, name, instant);
}
}
}
}
private void internalAddFile(Path path, byte[] bytes, Instant modified) {
Content content = contents.get(path);
if(content == null) {
content = new Content(modified);
contents.put(path, content);
}
if(content.modified.isAfter(modified) == false) {
content.bytes = bytes;
content.modified = modified;
Path parent;
while((parent = path.getParent()) != null) {
content = contents.get(parent);
if(content == null) {
content = new Content(modified);
contents.put(parent, content);
}
content.contained.add(path);
if(content.modified.isAfter(modified) || content.bytes != null) {
break;
}
content.modified = modified;
path = parent;
}
}
}
private void removeFileImplementation(Path path) {
Path root = ROOT.resolve(path);
Content folder = contents.get(root);
if(folder != null && folder.contained.notEmpty()) {
for(Path contentPath : folder.contained.toList()) {
removeRecursive(contentPath);
}
}
internalRemoveFile(root, now);
}
private void removeRecursive(Path root) {
Content folder = contents.get(root);
if(folder != null && folder.contained.notEmpty()) {
for(Path contentPath : folder.contained.toList()) {
removeRecursive(contentPath);
}
}
internalRemoveFile(root, now);
}
private void internalRemoveFile(Path path, Instant modified) {
Content content = contents.get(path);
if(content == null) {
return;
}
if(content.contained.notEmpty()) {
throw new IllegalArgumentException("Path " + path + " contains " + content.contained.size() + " items");
}
boolean remove = true;
Path parent;
while((parent = path.getParent()) != null) {
content = contents.get(parent);
if(content == null) {
throw new IllegalStateException("Parent " + parent + " of " + path + " not found");
}
if(remove) {
content.contained.remove(path);
remove = content.contained.isEmpty();
}
if(content.modified.isAfter(modified) || content.bytes != null) {
break;
}
content.modified = modified;
path = parent;
}
}
private List<Path> listFilesOrFoldersImplementation(Path path, boolean files) {
Content content = contents.get(ROOT.resolve(path));
if(content == null) {
return List.of();
}
return internalListFilesOrFolders(content, files).replaceAll(ROOT::relativize);
}
private List<Path> internalListFilesOrFolders(Content content, boolean files) {
ArrayList<Path> list = content.contained.toArrayList();
list.filter(p -> contents.get(p).bytes != null == files);
list.sort();
return list.toList();
}
private <E extends Exception> void writeSnapshot(
Path target,
Content content,
int level,
ExDoubleConsumer<E> monitor) throws IOException, E {
progress = 1;
progressTotal = contents.size() + 1;
String fileName = target.getFileName().toString();
if(isZIP(content, fileName)) {
Files.createDirectories(target.getParent());
writeZIP(target, content, level, monitor);
} else if(isGZIP(content, fileName)) {
Files.createDirectories(target.getParent());
writeGZIP(target, content, monitor);
} else {
writeFolder(target, ROOT, content, level, monitor);
}
}
private <E extends Exception> void writeFolder(
Path target,
Path root,
Content folder,
int level,
ExDoubleConsumer<E> monitor) throws IOException, E {
progress(monitor);
Files.createDirectories(target);
HashSet<Path> existing = Files.list(target).toSet();
for(Path path : folder.contained) {
String fileName = remap(path, level).getFileName().toString();
Path file = target.resolve(fileName);
Content content = contents.get(path);
boolean update = true;
boolean check = true;
if(Files.exists(file)) {
Instant instant = Files.getLastModifiedTime(file).toInstant();
update = instant.isBefore(content.modified);
check = instant.isAfter(content.modified) == false;
}
if(update || check) {
if(isZIP(content, fileName)) {
if(update) {
byte[] bytes = createZIP(path, content, level, monitor);
writeFile(file, bytes, content.modified);
}
} else if(isGZIP(content, fileName)) {
if(update) {
byte[] bytes = createGZIP(content);
writeFile(file, bytes, content.modified);
}
progressFolder(path, monitor);
} else if(content.bytes != null) {
if(update) {
byte[] bytes = content.bytes;
writeFile(file, bytes, content.modified);
}
progressFolder(path, monitor);
} else {
writeFolder(file, root, content, level, monitor);
}
}
existing.remove(file);
}
for(Path path : existing) {
if(Files.isDirectory(path)) {
ArrayList<Path> toBeDeleted = Files.walk(path).sorted(LONGEST_FIRST).toList();
for(Path toDelete : toBeDeleted) {
clean(toDelete);
}
} else {
clean(path);
}
}
Files.setLastModifiedTime(target, FileTime.from(folder.modified));
}
private static void clean(Path path) throws IOException {
try {
Files.delete(path);
} catch(DirectoryNotEmptyException e) {
System.out.println("\nCould not delete " + path + " because " + e.getReason());
}
}
private <E extends Exception> void writeGZIP(Path target, Content content, ExDoubleConsumer<E> monitor)
throws IOException, E {
progress(monitor);
byte[] bytes = createGZIP(content);
writeFile(target, bytes, content.modified);
progressFolder(target, monitor);
}
private byte[] createGZIP(Content content) throws IOException {
List<Path> gzFiles = internalListFilesOrFolders(content, true);
ByteArrayOutputStream bout = new ByteArrayOutputStream(1024 * 1024);
try(GZIPOutputStream gzip = new GZIPOutputStream(bout);) {
Content data2 = contents.get(gzFiles.get(0));
gzip.write(data2.bytes);
}
byte[] bytes = bout.toByteArray();
return bytes;
}
private static boolean isZIP(Content content, String fileName) {
return content.bytes == null && (fileName.endsWith(".jar") || fileName.endsWith(".zip"));
}
private boolean isGZIP(Content content, String fileName) {
return fileName.endsWith(".gz") &&
content.contained.size() == 1 &&
internalListFilesOrFolders(content, true).notEmpty();
}
private <E extends Exception> void writeZIP(Path target, Content content, int level, ExDoubleConsumer<E> monitor)
throws IOException, E {
target = remap(target, level);
byte[] bytes = createZIP(ROOT, content, level, monitor);
Instant modified = content.modified;
writeFile(target, bytes, modified);
}
private static void writeFile(Path target, byte[] bytes, Instant modified) throws IOException {
Files.write(target, bytes);
Files.setLastModifiedTime(target, FileTime.from(modified));
}
static int depth;
private <E extends Exception> byte[]
createZIP(Path target, Content content, int level, ExDoubleConsumer<E> monitor) throws IOException, E {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try(ZipOutputStream out = new ZipOutputStream(bout);) {
out.setLevel(level);
if(depth++ > 100) {
System.out.println("BREAK");
}
writeFolderToZIP(out, target, content, level, monitor);
} finally {
depth--;
}
return bout.toByteArray();
}
private <E extends Exception> void writeFolderToZIP(
ZipOutputStream zip,
Path root,
Content folder,
int level,
ExDoubleConsumer<E> monitor) throws IOException, E {
progress(monitor);
for(Path path : internalListFilesOrFolders(folder, true)) {
Path file = relative(root, remap(path, level));
Content data = contents.get(path);
writeFileToZIP(zip, file, data.bytes, data.modified);
progress(monitor);
}
for(Path path : internalListFilesOrFolders(folder, false)) {
Path file = relative(root, remap(path, level));
Content content = contents.get(path);
String fileName = file.getFileName().toString();
if(level > 0) {
if(fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
byte[] bytes = createZIP(path, content, level, monitor);
writeFileToZIP(zip, file, bytes, content.modified);
continue;
} else if(fileName.endsWith(".gz")) {
List<Path> gzFiles = internalListFilesOrFolders(content, true);
if(gzFiles.size() == 1) {
ByteArrayOutputStream bout = new ByteArrayOutputStream(1024 * 1024);
try(GZIPOutputStream gzip = new GZIPOutputStream(bout);) {
Content data2 = contents.get(gzFiles.get(0));
gzip.write(data2.bytes);
}
byte[] bytes = bout.toByteArray();
writeFileToZIP(zip, file, bytes, content.modified);
progressFolder(path, monitor);
continue;
}
}
}
ZipEntry entry = new ZipEntry(toUnixPath(file) + "/");
entry.setLastModifiedTime(FileTime.from(content.modified));
zip.putNextEntry(entry);
zip.closeEntry();
writeFolderToZIP(zip, root, content, level, monitor);
}
}
private <E extends Exception> void progressFolder(Path path, ExDoubleConsumer<E> monitor) throws E {
progress(monitor);
for(Path folder : listFolders(path)) {
progressFolder(folder, monitor);
}
for(Path folder : listFiles(path)) {
progressFolder(folder, monitor);
}
}
private <E extends Exception> void progress(ExDoubleConsumer<E> monitor) throws E {
monitor.accept(++progress / (double) progressTotal);
}
private Path remap(Path path, int level) throws IOException {
if(level == 0) {
return path;
}
path = ROOT.relativize(path);
for(IOFunction<Path, Path> remapper : remappers) {
path = remapper.apply(path);
}
return ROOT.resolve(path);
}
private static Path relative(Path root, Path path) {
Path relativize = root.relativize(path);
if(relativize.toString().contains("..")) {
throw new IllegalStateException("Path " + path + " not relative to " + root);
}
return relativize;
}
private static void writeFileToZIP(ZipOutputStream zip, Path file, byte[] bytes, Instant modified)
throws IOException {
ZipEntry entry = createEntry(file, modified);
zip.putNextEntry(entry);
zip.write(bytes);
zip.closeEntry();
}
private static ZipEntry createEntry(Path file, Instant modified) {
String name = toUnixPath(file);
ZipEntry entry = new ZipEntry(name);
FileTime lastModifiedTime = FileTime.from(modified);
entry.setLastModifiedTime(lastModifiedTime);
return entry;
}
private static String toUnixPath(Path path) {
return String.join("/", Stream.from(path).map(Path::toString).iterable());
}
}