|
55 | 55 | import java.io.File;
|
56 | 56 | import java.io.IOException;
|
57 | 57 | import java.io.InputStream;
|
| 58 | +import java.io.OutputStream; |
58 | 59 | import java.io.UnsupportedEncodingException;
|
59 | 60 | import java.net.URLDecoder;
|
| 61 | +import java.nio.file.Files; |
| 62 | +import java.nio.file.Path; |
| 63 | +import java.nio.file.StandardOpenOption; |
| 64 | +import java.nio.file.attribute.FileAttribute; |
60 | 65 | import java.security.KeyManagementException;
|
61 | 66 | import java.security.KeyStore;
|
62 | 67 | import java.security.KeyStoreException;
|
@@ -166,8 +171,45 @@ public static boolean isValidEmailAddress(String mail) {
|
166 | 171 |
|
167 | 172 | private static final PercentEscaper REPLAY_NAME_ENCODER = new PercentEscaper(".-_ ", false);
|
168 | 173 |
|
169 |
| - public static String replayNameToFileName(String replayName) { |
170 |
| - return REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr"; |
| 174 | + public static Path replayNameToPath(Path folder, String replayName) { |
| 175 | + // If we can, prefer directly using the replay name as the file name |
| 176 | + if (isUsable(folder, replayName + ".mcpr")) { |
| 177 | + return folder.resolve(replayName + ".mcpr"); |
| 178 | + } else { |
| 179 | + // otherwise, fall back to percent encoding |
| 180 | + return folder.resolve(REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr"); |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + /** |
| 185 | + * Checks whether a given file name is actually usable with the file system / operating system at the given folder. |
| 186 | + */ |
| 187 | + private static boolean isUsable(Path folder, String fileName) { |
| 188 | + Path path = folder.resolve(fileName); |
| 189 | + if (Files.exists(path)) { |
| 190 | + return true; // if it already exits, it's definitely usable |
| 191 | + } |
| 192 | + |
| 193 | + // Otherwise, there's no sure way to know, so we just gotta try |
| 194 | + try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)) { |
| 195 | + outputStream.flush(); |
| 196 | + } catch (IOException e) { |
| 197 | + return false; |
| 198 | + } |
| 199 | + |
| 200 | + // Looking good, but now we gotta clean up that mess (and Anti-Virus / Cloud Sync are know to lock them) |
| 201 | + int attempts = 0; |
| 202 | + while (true) { |
| 203 | + try { |
| 204 | + Files.delete(path); |
| 205 | + return true; |
| 206 | + } catch (IOException e) { |
| 207 | + if (attempts++ > 100) { |
| 208 | + LOGGER.warn("Repeatedly failed to clean up temporary test file at " + path + ": ", e); |
| 209 | + return false; // while we were able to use it, it's taken now and we can't get it back |
| 210 | + } |
| 211 | + } |
| 212 | + } |
171 | 213 | }
|
172 | 214 |
|
173 | 215 | public static String fileNameToReplayName(String fileName) {
|
@@ -356,4 +398,14 @@ public static <T> T configure(T instance, Consumer<T> configure) {
|
356 | 398 | configure.accept(instance);
|
357 | 399 | return instance;
|
358 | 400 | }
|
| 401 | + |
| 402 | + /** |
| 403 | + * Like {@link Files#createDirectories(Path, FileAttribute[])} but doesn't explode if it's a symlink. |
| 404 | + */ |
| 405 | + public static Path ensureDirectoryExists(Path path) throws IOException { |
| 406 | + // Who in their right mind thought the default behavior of throwing when the target is a link to a directory |
| 407 | + // was the preferred behavior?! Everyone has to fall for this at least once to learn it... |
| 408 | + // https://bugs.openjdk.java.net/browse/JDK-8130464 |
| 409 | + return Files.createDirectories(Files.exists(path) ? path.toRealPath() : path); |
| 410 | + } |
359 | 411 | }
|
0 commit comments