[FEATURE] Автоматическое создание ZIP архивов для директорий весом менее 32MB.

This commit is contained in:
Zaxar163 2019-07-03 16:14:49 +03:00
parent 493fbf8f2b
commit e30e749351
28 changed files with 97 additions and 125 deletions

View file

@ -31,12 +31,14 @@
import java.util.Timer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.CRC32;
import java.util.zip.ZipOutputStream;
import io.netty.handler.logging.LogLevel;
import pro.gravit.launcher.Launcher;
import pro.gravit.launcher.LauncherConfig;
import pro.gravit.launcher.NeedGarbageCollection;
import pro.gravit.launcher.hasher.HashedDir;
import pro.gravit.launcher.hasher.HashedEntry;
import pro.gravit.launcher.hwid.HWIDProvider;
import pro.gravit.launcher.managers.ConfigManager;
import pro.gravit.launcher.managers.GarbageManager;
@ -65,7 +67,6 @@
import pro.gravit.launchserver.components.Component;
import pro.gravit.launchserver.components.RegLimiterComponent;
import pro.gravit.launchserver.config.LaunchServerRuntimeConfig;
import pro.gravit.launchserver.dao.UserService;
import pro.gravit.launchserver.dao.provider.DaoProvider;
import pro.gravit.launchserver.manangers.*;
import pro.gravit.launchserver.manangers.hook.AuthHookManager;
@ -457,6 +458,8 @@ public static void main(String... args) throws Throwable {
public final Timer taskPool;
public final Path optimizedUpdatesDir;
public static Class<? extends LauncherBinary> defaultLauncherEXEBinaryClass = null;
public LaunchServer(Path dir, boolean testEnv, String[] args) throws IOException, InvalidKeySpecException {
@ -485,6 +488,7 @@ public LaunchServer(Path dir, boolean testEnv, String[] args) throws IOException
publicKeyFile = dir.resolve("public.key");
privateKeyFile = dir.resolve("private.key");
updatesDir = dir.resolve("updates");
optimizedUpdatesDir = dir.resolve("optimized_updates");
profilesDir = dir.resolve("profiles");
//Registration handlers and providers
@ -634,6 +638,9 @@ public LaunchServer(Path dir, boolean testEnv, String[] args) throws IOException
// Sync updates dir
if (!IOHelper.isDir(updatesDir))
Files.createDirectory(updatesDir);
if (!IOHelper.isDir(optimizedUpdatesDir))
Files.createDirectory(optimizedUpdatesDir);
updatesDirMap = null;
syncUpdatesDir(null);
// Sync profiles dir
@ -872,6 +879,7 @@ public void syncProfilesDir() throws IOException {
public void syncUpdatesDir(Collection<String> dirs) throws IOException {
LogHelper.info("Syncing updates dir");
boolean start = updatesDirMap == null;
Map<String, SignedObjectHolder<HashedDir>> newUpdatesDirMap = new HashMap<>(16);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(updatesDir)) {
for (final Path updateDir : dirStream) {
@ -894,16 +902,42 @@ public void syncUpdatesDir(Collection<String> dirs) throws IOException {
continue;
}
}
// Sync and sign update dir
LogHelper.info("Syncing '%s' update dir", name);
HashedDir updateHDir = new HashedDir(updateDir, null, true, true);
if (!start) processUpdate(updateDir, updateHDir, name);
newUpdatesDirMap.put(name, new SignedObjectHolder<>(updateHDir, privateKey));
}
}
updatesDirMap = Collections.unmodifiableMap(newUpdatesDirMap);
}
private void processUpdate(Path updateDir, HashedDir updateHDir, String name) throws IOException {
updateHDir.walk(IOHelper.CROSS_SEPARATOR, (path, filename, entry) -> {
if (entry.getType().equals(HashedEntry.Type.DIR)) {
if (entry.size() < IOHelper.MB32) {
Path p = updateDir.resolve(path);
Path out = optimizedUpdatesDir.resolve(name).resolve(path);
try (ZipOutputStream compressed = new ZipOutputStream(IOHelper.newOutput(out))) {
IOHelper.walk(p, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
compressed.putNextEntry(IOHelper.newZipEntry(
p.relativize(file).toString()
.replace(IOHelper.PLATFORM_SEPARATOR, IOHelper.CROSS_SEPARATOR)));
IOHelper.transfer(file, compressed);
return super.visitFile(file, attrs);
}
}, true);
}
return HashedDir.WalkAction.SKIP_DIR;
}
}
return HashedDir.WalkAction.CONTINUE;
});
}
public void restart() {
ProcessBuilder builder = new ProcessBuilder();
if (config.startScript != null) builder.command(Collections.singletonList(config.startScript));

View file

@ -1,7 +1,6 @@
package pro.gravit.launchserver.command.basic;
import org.bouncycastle.cert.X509CertificateHolder;
import pro.gravit.launcher.events.PingEvent;
import pro.gravit.launchserver.LaunchServer;
import pro.gravit.launchserver.command.Command;
import pro.gravit.launchserver.socket.handlers.NettyServerSocketHandler;
@ -9,7 +8,6 @@
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
public class TestCommand extends Command {
public TestCommand(LaunchServer server) {

View file

@ -3,8 +3,6 @@
import java.util.List;
import java.util.UUID;
import pro.gravit.launchserver.LaunchServer;
public class UserService {
private final UserDAO usersDao;

View file

@ -1,22 +1,16 @@
package pro.gravit.launchserver.manangers;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
@ -27,12 +21,10 @@
import pro.gravit.utils.helper.IOHelper;
import pro.gravit.utils.helper.SecurityHelper;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.spec.ECGenParameterSpec;
import java.time.Instant;
import java.time.LocalDate;

View file

@ -2,14 +2,11 @@
import com.google.gson.GsonBuilder;
import pro.gravit.launcher.hasher.HashedEntry;
import pro.gravit.launcher.hasher.HashedEntryAdapter;
import pro.gravit.launcher.hwid.HWID;
import pro.gravit.launcher.hwid.HWIDProvider;
import pro.gravit.launcher.managers.GsonManager;
import pro.gravit.launcher.request.JsonResultSerializeAdapter;
import pro.gravit.launcher.request.WebSocketEvent;
import pro.gravit.launcher.request.websockets.ClientWebSocketService;
import pro.gravit.launchserver.auth.handler.AuthHandler;
import pro.gravit.launchserver.auth.hwid.HWIDHandler;
import pro.gravit.launchserver.auth.permissions.PermissionsHandler;

View file

@ -1,6 +1,7 @@
package pro.gravit.launchserver.socket;
import java.net.InetSocketAddress;
import java.util.Arrays;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
@ -63,7 +64,7 @@ public void initChannel(SocketChannel ch) {
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));
if (server.config.netty.fileServerEnabled)
pipeline.addLast(new FileServerHandler(server.updatesDir, true));
pipeline.addLast(new FileServerHandler(Arrays.asList(server.updatesDir, server.optimizedUpdatesDir)));
pipeline.addLast(new WebSocketFrameHandler(context, server, service));
}
});

View file

@ -58,6 +58,7 @@ public WebSocketService(ChannelGroup channels, LaunchServer server) {
private static final HashMap<String, Class> responses = new HashMap<>();
private final Gson gson;
@SuppressWarnings("unchecked")
public void process(ChannelHandlerContext ctx, TextWebSocketFrame frame, Client client, String ip) {
String request = frame.text();
WebSocketServerResponse response = gson.fromJson(request, WebSocketServerResponse.class);

View file

@ -23,7 +23,7 @@
import pro.gravit.launchserver.socket.LauncherNettyServer;
import pro.gravit.utils.helper.LogHelper;
@SuppressWarnings({"unused", "rawtypes"})
@SuppressWarnings("unused")
public final class NettyServerSocketHandler implements Runnable, AutoCloseable {
private SSLServerSocketFactory ssf;

View file

@ -2,22 +2,17 @@
import java.util.concurrent.TimeUnit;
import com.google.gson.GsonBuilder;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import pro.gravit.launchserver.LaunchServer;
import pro.gravit.launchserver.socket.Client;
import pro.gravit.launchserver.socket.NettyConnectContext;
import pro.gravit.launchserver.socket.WebSocketService;
import pro.gravit.utils.helper.CommonHelper;
import pro.gravit.utils.helper.IOHelper;
import pro.gravit.utils.helper.LogHelper;

View file

@ -3,7 +3,6 @@
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
@ -22,13 +21,13 @@
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.TimeZone;
import java.util.regex.Pattern;
import javax.activation.MimetypesFileTypeMap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
@ -48,6 +47,8 @@
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import pro.gravit.utils.helper.IOHelper;
import pro.gravit.utils.helper.LogHelper;
public class FileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@ -56,12 +57,10 @@ public class FileServerHandler extends SimpleChannelInboundHandler<FullHttpReque
public static final String READ = "r";
public static final int HTTP_CACHE_SECONDS = 60;
private static final boolean OLD_ALGO = Boolean.parseBoolean(System.getProperty("launcher.fileserver.oldalgo", "true"));
private final Path base;
private final boolean fullOut;
private final List<Path> base;
public FileServerHandler(Path base, boolean fullOut) {
public FileServerHandler(List<Path> base) {
this.base = base;
this.fullOut = fullOut;
}
@Override
@ -83,13 +82,13 @@ public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) thr
return;
}
File file = base.resolve(path).toFile();
if (file.isHidden() || !file.exists()) {
Optional<File> fileO = base.stream().map(t -> t.resolve(path)).filter(t -> IOHelper.isFile(t) && !IOHelper.isHidden(t)).map(t -> t.toFile()).findFirst();
if (!fileO.isPresent()) {
sendError(ctx, NOT_FOUND);
return;
}
if (file.isDirectory()) {
File file = fileO.get();
/*if (file.isDirectory()) {
if (fullOut) {
if (uri.endsWith("/")) {
sendListing(ctx, file, uri);
@ -98,12 +97,12 @@ public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) thr
}
} else sendError(ctx, NOT_FOUND); // can not handle dirs
return;
}
}*/
if (!file.isFile()) {
/*if (!file.isFile()) {
sendError(ctx, FORBIDDEN);
return;
}
}*/
// Cache Validation
String ifModifiedSince = request.headers().get(HttpHeaderNames.IF_MODIFIED_SINCE);
@ -166,14 +165,12 @@ public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) thr
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
LogHelper.error(cause);
if (ctx.channel().isActive()) {
sendError(ctx, INTERNAL_SERVER_ERROR);
}
}
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private static String sanitizeUri(String uri) {
// Decode the path.
try {
@ -192,60 +189,6 @@ private static String sanitizeUri(String uri) {
return Paths.get(uri).normalize().toString().substring(1);
}
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");
private static void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
StringBuilder buf = new StringBuilder()
.append("<!DOCTYPE html>\r\n")
.append("<html><head><meta charset='utf-8' /><title>")
.append("Listing of: ")
.append(dirPath)
.append("</title></head><body>\r\n")
.append("<h3>Listing of: ")
.append(dirPath)
.append("</h3>\r\n")
.append("<ul>")
.append("<li><a href=\"../\">..</a></li>\r\n");
for (File f : dir.listFiles()) {
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}
buf.append("<li><a href=\"")
.append(name)
.append("\">")
.append(name)
.append("</a></li>\r\n");
}
buf.append("</ul></body></html>\r\n");
ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(HttpHeaderNames.LOCATION, newUri);
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(
HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));

View file

@ -36,10 +36,17 @@ public interface DownloadTotalCallback {
public static class DownloadTask {
public String apply;
public long size;
public boolean isZip;
public DownloadTask(String apply, long size) {
this.apply = apply;
this.size = size;
isZip = false;
}
public DownloadTask(String apply, long size, boolean isZip) {
this.apply = apply;
this.size = size;
this.isZip = isZip;
}
}
@ -59,7 +66,7 @@ public void download(String base, List<DownloadTask> applies, Path dstDirFile, D
get.reset();
get.setURI(u);
}
httpclient.execute(get, new FileDownloadResponseHandler(targetPath, apply, callback, totalCallback, false));
httpclient.execute(get, new FileDownloadResponseHandler(targetPath, apply, callback, totalCallback));
}
}
}
@ -118,12 +125,12 @@ public FileDownloadResponseHandler(Path target) {
totalCallback = null;
}
public FileDownloadResponseHandler(Path target, DownloadTask task, DownloadCallback callback, DownloadTotalCallback totalCallback, boolean zip) {
public FileDownloadResponseHandler(Path target, DownloadTask task, DownloadCallback callback, DownloadTotalCallback totalCallback) {
this.target = target;
this.task = task;
this.callback = callback;
this.totalCallback = totalCallback;
this.zip = zip;
this.zip = task != null ? task.isZip : false;
}
public FileDownloadResponseHandler(Path target, DownloadCallback callback, DownloadTotalCallback totalCallback, boolean zip) {
@ -137,16 +144,6 @@ public FileDownloadResponseHandler(Path target, DownloadCallback callback, Downl
@Override
public Path handleResponse(HttpResponse response) throws IOException {
InputStream source = response.getEntity().getContent();
int returnCode = response.getStatusLine().getStatusCode();
if(returnCode != 200)
{
throw new IllegalStateException(String.format("Request download file %s return code %d", target.toString(), returnCode));
}
long contentLength = response.getEntity().getContentLength();
if (task != null && contentLength != task.size)
{
LogHelper.warning("Missing content length: expected %d | found %d", task.size, contentLength);
}
if (zip) {
try (ZipInputStream input = IOHelper.newZipInput(source)) {
ZipEntry entry = input.getNextEntry();
@ -171,13 +168,14 @@ public Path handleResponse(HttpResponse response) throws IOException {
}
if (callback != null && task != null) {
callback.stateChanged(task.apply, 0, task.size);
transfer(source, this.target, task.apply, task.size, callback, totalCallback);
transfer(source, target, task.apply, task.size < 0 ? source.available() : task.size, callback, totalCallback);
} else
IOHelper.transfer(source, this.target);
IOHelper.transfer(source, IOHelper.newOutput(target));
return this.target;
}
}
public static void transfer(InputStream input, Path file, String filename, long size, DownloadCallback callback, DownloadTotalCallback totalCallback) throws IOException {
try (OutputStream fileOutput = IOHelper.newOutput(file)) {
long downloaded = 0L;

View file

@ -4,6 +4,7 @@
//Набор стандартных событий
public class ControlEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("f1051a64-0cd0-4ed8-8430-d856a196e91f");
public enum ControlCommand {

View file

@ -1,7 +1,5 @@
package pro.gravit.launcher.events.request;
import java.util.UUID;
import pro.gravit.launcher.ClientPermissions;
import pro.gravit.launcher.LauncherNetworkAPI;
import pro.gravit.launcher.events.RequestEvent;

View file

@ -1,7 +1,5 @@
package pro.gravit.launcher.events.request;
import java.util.UUID;
import pro.gravit.launcher.LauncherNetworkAPI;
import pro.gravit.launcher.events.RequestEvent;
import pro.gravit.launcher.profiles.PlayerProfile;

View file

@ -8,6 +8,7 @@
public class CheckServerRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID _uuid = UUID.fromString("8801d07c-51ba-4059-b61d-fe1f1510b28a");
@LauncherNetworkAPI
public UUID uuid;

View file

@ -7,6 +7,7 @@
public class JoinServerRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("2a12e7b5-3f4a-4891-a2f9-ea141c8e1995");
public JoinServerRequestEvent(boolean allow) {

View file

@ -7,6 +7,7 @@
public class LauncherRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("d54cc12a-4f59-4f23-9b10-f527fdd2e38f");
@LauncherNetworkAPI
public String url;

View file

@ -8,6 +8,7 @@
public class ProfileByUUIDRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("b9014cf3-4b95-4d38-8c5f-867f190a18a0");
@LauncherNetworkAPI
public String error;

View file

@ -8,6 +8,7 @@
public class ProfileByUsernameRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("06204302-ff6b-4779-b97d-541e3bc39aa1");
@LauncherNetworkAPI
public String error;

View file

@ -9,6 +9,7 @@
public class ProfilesRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("2f26fbdf-598a-46dd-92fc-1699c0e173b1");
@LauncherNetworkAPI
public List<ClientProfile> profiles;

View file

@ -8,6 +8,7 @@
public class SetProfileRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("08c0de9e-4364-4152-9066-8354a3a48541");
@LauncherNetworkAPI
public ClientProfile newProfile;

View file

@ -8,6 +8,7 @@
public class UpdateListRequestEvent extends RequestEvent {
@SuppressWarnings("unused")
private static final UUID uuid = UUID.fromString("5fa836ae-6b61-401c-96ac-d8396f07ec6b");
@LauncherNetworkAPI
public final HashSet<String> dirs;

View file

@ -4,7 +4,6 @@
import pro.gravit.launcher.LauncherAPI;
import pro.gravit.launcher.LauncherNetworkAPI;
import pro.gravit.launcher.events.request.AuthRequestEvent;
import pro.gravit.launcher.hwid.OshiHWID;
import pro.gravit.launcher.request.Request;
import pro.gravit.launcher.request.websockets.WebSocketRequest;
import pro.gravit.utils.helper.VerifyHelper;

View file

@ -210,6 +210,10 @@ public UpdateRequestEvent requestDo(StandartClientWebSocketService service) thro
} catch (IOException ex) {
LogHelper.error(ex);
}
if (entry.size() < IOHelper.MB32) {
adds.add(new ListDownloader.DownloadTask(path, -1, true));
return HashedDir.WalkAction.SKIP_DIR;
}
}
return HashedDir.WalkAction.CONTINUE;
});

View file

@ -3,7 +3,6 @@
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import javax.net.ssl.SSLException;

View file

@ -7,13 +7,10 @@
import javax.net.ssl.SSLException;
import com.google.gson.GsonBuilder;
import pro.gravit.launcher.events.request.ErrorRequestEvent;
import pro.gravit.launcher.request.Request;
import pro.gravit.launcher.request.RequestException;
import pro.gravit.launcher.request.WebSocketEvent;
import pro.gravit.utils.helper.CommonHelper;
import pro.gravit.utils.helper.JVMHelper;
import pro.gravit.utils.helper.LogHelper;

View file

@ -350,7 +350,7 @@ public void walk(CharSequence separator, WalkCallback callback) throws IOExcepti
}
public enum WalkAction {
STOP, CONTINUE
STOP, SKIP_DIR, CONTINUE
}
@FunctionalInterface
@ -375,7 +375,9 @@ private WalkAction walk(String append, CharSequence separator, WalkCallback call
else newAppend = append + separator + entry.getKey();
WalkAction a = callback.walked(newAppend, entry.getKey(), e);
if (a == WalkAction.STOP) return a;
if (a == WalkAction.CONTINUE)
a = ((HashedDir) e).walk(newAppend, separator, callback, false);
else a = WalkAction.CONTINUE; // skip
if (a == WalkAction.STOP) return a;
}
}

View file

@ -297,6 +297,15 @@ public static boolean isFile(Path path) {
return Files.isRegularFile(path, LINK_OPTIONS);
}
@LauncherAPI
public static boolean isHidden(Path path) {
try {
return Files.isHidden(path);
} catch (IOException e) {
return false;
}
}
@LauncherAPI
public static boolean isValidFileName(String fileName) {
return !fileName.equals(".") && !fileName.equals("..") &&