001/******************************************************************************* 002The MIT License (MIT) 003 004Copyright (c) 2024 KILLCODING.COM 005 006Permission is hereby granted, free of charge, to any person obtaining a copy 007of this software and associated documentation files (the "Software"), to deal 008in the Software without restriction, including without limitation the rights 009to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010copies of the Software, and to permit persons to whom the Software is 011furnished to do so, subject to the following conditions: 012 013The above copyright notice and this permission notice shall be included in 014all copies or substantial portions of the Software. 015 016THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 022THE SOFTWARE. 023*****************************************************************************/ 024package com.killcoding.file; 025 026import java.io.File; 027import com.killcoding.log.LoggerFactory; 028import com.killcoding.log.Logger; 029import java.io.IOException; 030import java.util.List; 031import java.util.ArrayList; 032import java.nio.ByteBuffer; 033import java.io.ByteArrayInputStream; 034import java.sql.Timestamp; 035import com.killcoding.tool.CommonTools; 036import java.nio.file.Path; 037import java.nio.file.Files; 038import java.nio.file.Paths; 039import java.text.SimpleDateFormat; 040import java.time.ZonedDateTime; 041import java.time.ZoneId; 042import java.time.LocalDateTime; 043import java.time.format.DateTimeFormatter; 044import com.killcoding.cache.CacheArrayFilter; 045import java.nio.file.attribute.BasicFileAttributes; 046import java.nio.file.attribute.FileTime; 047import java.nio.file.attribute.FileAttribute; 048import java.nio.file.LinkOption; 049 050public abstract class BaseFile { 051 052 protected static final String TMP_MATCHES_EDIT = "^.*\\..*~$"; 053 protected static final String TMP_MATCHES_VI_SWP = "^.*\\..*\\.sw[xp]$"; 054 055 protected static final String TMP_EXT_MODIFY_FILE = ".modify.tmp~"; 056 protected static final String TMP_EXT_ACCESS_FILE = ".access.tmp~"; 057 protected static final String TMP_EXT_CHECK_FILE = ".check.tmp~"; 058 059 protected static final String TMP_WRITING_SWP = "^.*\\.writing.tmp~$"; 060 protected static final String TMP_WRITING_FOR_REPLACE = "\\.writing\\.tmp~"; 061 protected static final String TMP_WRITING_FILE = ".writing.tmp~"; 062 063 public static String CHARSET = "UTF-8"; 064 065 public abstract boolean isFile() throws IOException; 066 067 public abstract boolean isDirectory() throws IOException; 068 069 public abstract boolean isLink() throws IOException; 070 071 public abstract boolean delete() throws IOException; 072 073 public abstract boolean exists() throws IOException; 074 075 public abstract boolean mkdirs() throws IOException; 076 077 public abstract boolean createLink(String target) throws IOException; 078 079 public abstract String readLink() throws IOException; 080 081 public abstract boolean write(byte[] data, boolean append) throws IOException; 082 083 public abstract boolean write(byte[] data) throws IOException; 084 085 public abstract boolean write(String data, boolean append) throws IOException; 086 087 public abstract boolean write(String data) throws IOException; 088 089 public abstract byte[] readAllBytes() throws IOException; 090 091 public abstract String readAllString() throws IOException; 092 093 public abstract long size() throws IOException; 094 095 public abstract boolean complete() throws IOException; 096 097 public abstract Timestamp getModifyTime() throws IOException; 098 099 public abstract void setModifyTime(Timestamp modifyTime) throws IOException; 100 101 public abstract void copyTo(String toPath) throws IOException; 102 103 public abstract void moveTo(String toPath) throws IOException; 104 105 public abstract boolean beforeDelete(boolean realDeleted); 106 107 public abstract void afterDelete(boolean realDeleted); 108 109 public abstract boolean beforeMkdirs(); 110 111 public abstract void afterMkdirs(); 112 113 public abstract boolean beforeCreateLink(String target); 114 115 public abstract void afterCreateLink(String target); 116 117 public abstract boolean beforeWrite(); 118 119 public abstract void afterWrite(); 120 121 public static long CACHE_ARRAY_FILTER_TIMER = 10L; 122 123 public static long LOGIC_TIMEOUT_MS = 300000L; 124 125 public static long LOGIC_ACCESS_TIMEOUT_MS = 60000L; 126 127 public static long LOGIC_CHECK_TIMEOUT_MS = 60000L; 128 129 protected static boolean stopSync = true; 130 131 protected File syncRoot = null; 132 133 protected File origin; 134 135 public static Path TEMP_DIR = null; 136 137 private static long debugLogTime = 0L; 138 139 public static final List<String> SYNC_PATH_ALLOWED = new ArrayList<String>(); 140 public static final List<String> SYNC_PATH_IGNORED = new ArrayList<String>(); 141 142 static { 143 try{ 144 SYNC_PATH_IGNORED.add(TMP_MATCHES_EDIT); 145 SYNC_PATH_IGNORED.add(TMP_MATCHES_VI_SWP); 146 147 String tmpdir = System.getProperty("java.io.tmpdir"); 148 Path tmpBfDir = Paths.get(String.format("%s/BaseFile",tmpdir)); 149 if(!Files.exists(tmpBfDir)){ 150 Files.createDirectories(tmpBfDir); 151 } 152 Path pidDir = Paths.get(String.format("%s/%s/",tmpBfDir.toString(),ProcessHandle.current().pid())); 153 if(TEMP_DIR == null) TEMP_DIR = Files.createDirectories(pidDir); 154 155 }catch(IOException e){ 156 Logger.systemError(BaseFile.class,e.getMessage(),e); 157 } 158 } 159 160 public BaseFile(String path) { 161 super(); 162 origin = new File(path); 163 } 164 165 public static void setTmpDir(String tmpDir) throws IOException { 166 TEMP_DIR = Files.createDirectories(Paths.get(tmpDir)); 167 } 168 169 public String getPath() { 170 return parsePath(String.format("%s/%s",getParent(),origin.getName())); 171 } 172 173 public File getOrigin(){ 174 return origin; 175 } 176 177 public String getParent() { 178 String parent = CommonTools.isBlank(origin.getParent()) ? "/" : origin.getParent(); 179 return replacePath(parent); 180 } 181 182 public List<String> getParents() { 183 List<String> parents = new ArrayList<String>(); 184 String path = getPath(); 185 do{ 186 File file = new File(path); 187 path = file.getParent(); 188 189 if(path == null) break; 190 191 parents.add(path); 192 }while(path != null); 193 194 return parents; 195 } 196 197 public void write(int partSize,byte[] allBytes) throws IOException { 198 try{ 199 ByteBuffer bf = ByteBuffer.allocate(partSize); 200 ByteArrayInputStream bais = new ByteArrayInputStream(allBytes); 201 int partIndex = -1; 202 while(bais.read() != -1){ 203 bf.flip(); 204 partIndex += 1; 205 List<Byte> parts = new ArrayList<Byte>(); 206 while (bf.hasRemaining()) { 207 parts.add(bf.get()); 208 } 209 bf.clear(); 210 int partsSize = parts.size(); 211 byte[] part = new byte[partsSize]; 212 for (int i = 0; i < partsSize; i++) { 213 part[i] = parts.get(i); 214 } 215 write(part,true); 216 } 217 }finally{ 218 complete(); 219 } 220 } 221 222 protected String toModifyTmpFilePath() throws IOException { 223 if(syncRoot == null) throw new IOException("The 'BaseFile.syncRoot' is null."); 224 225 File syncFile = new File(getPath()); 226 if(syncRoot != null){ 227 String syncRootParent = replacePath(syncRoot.getParent()) + "/"; 228 String replacedFilePath = TEMP_DIR.toAbsolutePath() + "/." + replacePath(syncFile.getAbsolutePath()).replaceFirst(syncRootParent,"") + TMP_EXT_MODIFY_FILE; 229 return replacedFilePath; 230 } 231 return null; 232 } 233 234 protected String toAccessTmpFilePath() throws IOException { 235 if(syncRoot == null) throw new IOException("The 'BaseFile.syncRoot' is null."); 236 237 File syncFile = new File(getPath()); 238 if(syncRoot != null){ 239 String syncRootParent = replacePath(syncRoot.getParent()) + "/"; 240 String replacedFilePath = TEMP_DIR.toAbsolutePath() + "/." + replacePath(syncFile.getAbsolutePath()).replaceFirst(syncRootParent,"") + TMP_EXT_ACCESS_FILE; 241 return replacedFilePath; 242 } 243 return null; 244 } 245 246 protected String toCheckTmpFilePath() throws IOException { 247 if(syncRoot == null) throw new IOException("The 'BaseFile.syncRoot' is null."); 248 249 File syncFile = new File(getPath()); 250 if(syncRoot != null){ 251 String syncRootParent = replacePath(syncRoot.getParent()) + "/"; 252 String replacedFilePath = TEMP_DIR.toAbsolutePath() + "/." + replacePath(syncFile.getAbsolutePath()).replaceFirst(syncRootParent,"") + TMP_EXT_CHECK_FILE; 253 return replacedFilePath; 254 } 255 return null; 256 } 257 258 public synchronized static void stopSync() { 259 LoggerFactory.getLogger(DiskFile.class).mark("Sending stop command."); 260 stopSync = true; 261 for (int i = 0; i < 29; i++) { 262 try{ 263 if(!isStopSync()) { 264 stopSync(); 265 break; 266 } 267 Thread.sleep(1000L); 268 }catch(InterruptedException e){ 269 LoggerFactory.getLogger(BaseFile.class).error(e.getMessage(),e); 270 } 271 } 272 LoggerFactory.getLogger(DiskFile.class).mark("Sent stop command"); 273 } 274 275 public static boolean isStopSync(){ 276 DiskFile.checkQueuePathMapping(); 277 RemoteFile.checkQueuePathMapping(); 278 return stopSync && DiskFile.getQueuePathMapping().isEmpty() && RemoteFile.getQueuePathMapping().isEmpty(); 279 } 280 281 protected static boolean isDebug(RemoteFile remoteFile){ 282 boolean debug = false; 283 if(remoteFile != null){ 284 boolean fileDebug = remoteFile.getOrigin().getName().matches("^(\\.{0,1}DEBUG[\\.\\d\\w]*)$"); 285 boolean logDeubg = remoteFile.getConfigProperties().getBoolean("LogCompletedStatus", false); 286 debug = fileDebug || logDeubg; 287 } 288 return debug || LoggerFactory.getLogger(BaseFile.class).isDebugEnabled(); 289 } 290 291 public static boolean isHidden(String path) throws Exception{ 292 Path checkPath = Paths.get(path); 293 294 if(!Files.exists(checkPath)) return false; 295 296 while(checkPath != null){ 297 298 boolean hide = Files.isHidden(checkPath); 299 300 if(hide) return true; 301 302 checkPath = checkPath.getParent(); 303 304 305 } 306 return false; 307 } 308 309 public static boolean checkSyncPathAvailable(String path, boolean showHidden,boolean showLink,long maxFileSize, List<String> syncPathAllowed, 310 List<String> syncPathIgnored) { 311 312 if(maxFileSize <= 0) return false; 313 314 try { 315 boolean matchedAllowed = true; 316 boolean matchedIgnored = false; 317 Path fullPath = Paths.get(path); 318 319 if(!showHidden){ 320 matchedAllowed = !isHidden(path); 321 } 322 323 if(matchedAllowed && !showLink && Files.exists(fullPath)){ 324 boolean isLink = Files.isSymbolicLink(fullPath); 325 matchedAllowed = !isLink; 326 } 327 328 if (matchedAllowed && syncPathAllowed != null) { 329 for (String regex : syncPathAllowed) { 330 if (!CommonTools.isBlank(regex)) { 331 matchedAllowed = path.matches(regex); 332 333 if (matchedAllowed) 334 break; 335 } 336 } 337 } 338 if (matchedAllowed && syncPathIgnored != null) { 339 for (String regex : syncPathIgnored) { 340 if (!CommonTools.isBlank(regex)) { 341 matchedIgnored = path.matches(regex); 342 343 if (matchedIgnored) 344 break; 345 } 346 } 347 } 348 349 boolean b = matchedAllowed && !matchedIgnored; 350 if (b) { 351 Path checkPath = Paths.get(path); 352 if (Files.exists(checkPath)) { 353 354 int parentLength = checkPath.getParent().toString().length(); 355 int fileNamelength = checkPath.getFileName().toString().length(); 356 357 b = parentLength <= RemoteFile.MAX_FILE_PARENT_PATH_LENGTH && fileNamelength <= RemoteFile.MAX_FILE_NAME_LENGTH; 358 359 if (b && Files.isRegularFile(checkPath)) { 360 long fileSize = Files.size(checkPath); 361 b = fileSize <= maxFileSize; 362 } 363 }else{ 364 b = false; 365 } 366 } 367 368 if(b) { 369 Path checkPath = Paths.get(path); 370 if (Files.exists(checkPath)) { 371 boolean isLink = Files.isSymbolicLink(checkPath); 372 boolean isFile = checkPath.toFile().isFile(); 373 if(isFile && !isLink){ 374 if(RemoteFile.SYNC_CUTOFF_TIME == null){ 375 b = false; 376 } else{ 377 long syncCutoffTimeMs = 0L; 378 syncCutoffTimeMs = RemoteFile.SYNC_CUTOFF_TIME.getTime(); 379 BasicFileAttributes attr = Files.readAttributes(checkPath, BasicFileAttributes.class); 380 FileTime fileTime = attr.lastModifiedTime(); 381 382 b = fileTime.toMillis() >= syncCutoffTimeMs; 383 } 384 } 385 }else{ 386 b = false; 387 } 388 } 389 390 if (!b) { 391 LoggerFactory.getLogger(BaseFile.class).debug("Unavailable: {}", path); 392 } 393 return b; 394 } catch (Exception e) { 395 LoggerFactory.getLogger(BaseFile.class).error(e.getMessage(), e); 396 return false; 397 } 398 } 399 400 public boolean isDir() throws IOException { 401 return isDirectory(); 402 } 403 404 public Timestamp timeZoneConver(String destTimeZone, String srcTimeZone, Timestamp srcDate) throws IOException { 405 try{ 406 String pattern = "yyyy-MM-dd HH:mm:ss.SSS"; 407 SimpleDateFormat fullFormat = new SimpleDateFormat(pattern); 408 DateTimeFormatter format = DateTimeFormatter.ofPattern(pattern); 409 String srcDateStr = fullFormat.format(srcDate); 410 LocalDateTime ldt = LocalDateTime.parse(srcDateStr, DateTimeFormatter.ofPattern(pattern)); 411 ZoneId srcZoneId = ZoneId.of(srcTimeZone); 412 ZonedDateTime srcDateTime = ldt.atZone(srcZoneId); 413 ZoneId destZoneId = ZoneId.of(destTimeZone); 414 ZonedDateTime destDateTime = srcDateTime.withZoneSameInstant(destZoneId); 415 return new Timestamp(fullFormat.parse(format.format(destDateTime)).getTime()); 416 }catch(Exception e){ 417 throw new IOException(e.getMessage(),e); 418 } 419 } 420 421 protected static String replacePath(String source){ 422 return source.replaceAll("\\\\", "/"); 423 } 424 425 protected static String parsePath(String source){ 426 return source.replaceAll("\\\\", "/").replaceAll("//","/"); 427 } 428 429 public static boolean checkUsage(RemoteFile rf){ 430 return memoryUsage() < RemoteFile.MAX_MEMORY_USAGE && rf.getDataSourceWriter().getUsage() < RemoteFile.MAX_CONNECT_USAGE && rf.getDataSourceReader().getUsage() < RemoteFile.MAX_CONNECT_USAGE; 431 } 432 433 434 public static long memoryUsing(){ 435 int mb = 1024 * 1024; 436 Runtime instance = Runtime.getRuntime(); 437 long totalMemory = instance.totalMemory() / mb; 438 long freeMemory = instance.freeMemory() / mb; 439 return (instance.totalMemory() - instance.freeMemory()) / mb; 440 } 441 442 public static double memoryUsage(long using){ 443 int mb = 1024 * 1024; 444 Runtime instance = Runtime.getRuntime(); 445 long maxMemory = instance.maxMemory() / mb; 446 return (using * 1.0D / maxMemory * 1.0D); 447 } 448 449 public static double memoryUsage(){ 450 return memoryUsage(memoryUsing()); 451 } 452 453 protected synchronized static void debugLog(Class clazz,String point,RemoteFile rf,CacheArrayFilter filter){ 454 long ctm = System.currentTimeMillis(); 455 boolean canWrite = (ctm - debugLogTime) >= 60000L; 456 if(canWrite){ 457 DiskFile.checkQueuePathMapping(); 458 RemoteFile.checkQueuePathMapping(); 459 debugLogTime = ctm; 460 long memoryUsing = memoryUsing(); 461 String readerUsage = String.format("%.2f%%",rf.getDataSourceReader().getUsage()*100); 462 String writerUsage = String.format("%.2f%%",rf.getDataSourceWriter().getUsage()*100); 463 String memoryUsage = String.format("%.2f%%",memoryUsage(memoryUsing)*100); 464 LoggerFactory.getLogger(clazz).mark("************{}************",point); 465 LoggerFactory.getLogger(clazz).mark("[{}] DiskQueuePathMapping - {}",point,DiskFile.getQueuePathMapping().keySet().size()); 466 LoggerFactory.getLogger(clazz).mark("[{}] RemoteQueuePathMapping - {}",point,RemoteFile.getQueuePathMapping().keySet().size()); 467 LoggerFactory.getLogger(clazz).mark("[{}] CacheArrayPoolUsage - {}",point,filter.getCacheArray().getUsingPoolSize()); 468 LoggerFactory.getLogger(clazz).mark("[{}] DataSourceReaderUsage - {}",point,readerUsage); 469 LoggerFactory.getLogger(clazz).mark("[{}] DataSourceWriterUsage - {}",point,writerUsage); 470 LoggerFactory.getLogger(clazz).mark("[{}] MemoryUsage - {}mb ({})",point,memoryUsing,memoryUsage); 471 } 472 } 473 474}