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_CHECK_TIMEOUT_MS = 60000L;
126        
127        protected static boolean stopSync = true;
128        
129        protected File syncRoot = null;
130
131        protected File origin;
132        
133        public static Path TEMP_DIR = null;
134        
135        private static long debugLogTime = 0L;
136        
137        public static final List<String> SYNC_PATH_ALLOWED = new ArrayList<String>();
138        public static final List<String> SYNC_PATH_IGNORED = new ArrayList<String>();   
139        
140        static {
141            try{
142                SYNC_PATH_IGNORED.add(TMP_MATCHES_EDIT);
143                SYNC_PATH_IGNORED.add(TMP_MATCHES_VI_SWP);
144                
145                String tmpdir = System.getProperty("java.io.tmpdir");
146                Path tmpBfDir = Paths.get(String.format("%s/BaseFile",tmpdir));
147                if(!Files.exists(tmpBfDir)){
148                    Files.createDirectories(tmpBfDir);
149                }
150                Path pidDir = Paths.get(String.format("%s/%s/",tmpBfDir.toString(),ProcessHandle.current().pid()));
151                if(TEMP_DIR == null) TEMP_DIR = Files.createDirectories(pidDir);
152                
153            }catch(IOException e){
154                Logger.systemError(BaseFile.class,e.getMessage(),e);
155            }
156        }
157
158        public BaseFile(String path) {
159                super();
160                origin = new File(path);
161        }
162        
163        public static void setTmpDir(String tmpDir) throws IOException {
164            TEMP_DIR = Files.createDirectories(Paths.get(tmpDir));
165        }
166
167        public String getPath() {
168        return parsePath(String.format("%s/%s",getParent(),origin.getName()));
169        }
170        
171        public File getOrigin(){
172            return origin;
173        }
174        
175        public String getParent() {
176                String parent = CommonTools.isBlank(origin.getParent()) ? "/" : origin.getParent();
177        return replacePath(parent);
178        }
179        
180        public List<String> getParents() {
181            List<String> parents = new ArrayList<String>();
182            String path = getPath();
183            do{
184                File file = new File(path);
185                path = file.getParent();
186                
187                if(path == null) break;
188                
189            parents.add(path);
190            }while(path != null);
191            
192            return parents;
193        }
194        
195        public void write(int partSize,byte[] allBytes) throws IOException {
196            try{
197            ByteBuffer bf = ByteBuffer.allocate(partSize);
198            ByteArrayInputStream bais = new ByteArrayInputStream(allBytes);
199            int partIndex = -1;
200            while(bais.read() != -1){
201                bf.flip();
202                        partIndex += 1;
203                        List<Byte> parts = new ArrayList<Byte>();
204                        while (bf.hasRemaining()) {
205                                parts.add(bf.get());
206                        }
207                        bf.clear();
208                        int partsSize = parts.size();
209                        byte[] part = new byte[partsSize];
210                        for (int i = 0; i < partsSize; i++) {
211                                part[i] = parts.get(i);
212                        }
213                        write(part,true);
214            }
215            }finally{
216                complete();
217            }
218        }       
219        
220    protected String toModifyTmpFilePath(){
221        if(syncRoot == null) return null;
222        
223        File syncFile = new File(getPath());
224        if(syncRoot != null){
225            String syncRootParent = replacePath(syncRoot.getParent()) + "/";
226            String replacedFilePath = TEMP_DIR.toAbsolutePath() + "/." + replacePath(syncFile.getAbsolutePath()).replaceFirst(syncRootParent,"") + TMP_EXT_MODIFY_FILE;
227            return replacedFilePath;
228        }
229        return null;  
230    }  
231    
232    protected String toAccessTmpFilePath(){
233        if(syncRoot == null) return null;
234        
235        File syncFile = new File(getPath());
236        if(syncRoot != null){
237            String syncRootParent = replacePath(syncRoot.getParent()) + "/";
238            String replacedFilePath = TEMP_DIR.toAbsolutePath() + "/." + replacePath(syncFile.getAbsolutePath()).replaceFirst(syncRootParent,"") + TMP_EXT_ACCESS_FILE;
239            return replacedFilePath;
240        }
241        return null;            
242    }  
243
244    protected String toCheckTmpFilePath(){
245        if(syncRoot == null) return null;
246        
247        File syncFile = new File(getPath());
248        if(syncRoot != null){
249            String syncRootParent = replacePath(syncRoot.getParent()) + "/";
250            String replacedFilePath = TEMP_DIR.toAbsolutePath() + "/." + replacePath(syncFile.getAbsolutePath()).replaceFirst(syncRootParent,"") + TMP_EXT_CHECK_FILE;
251            return replacedFilePath;
252        }
253        return null;            
254    }  
255        public synchronized static void stopSync() {
256                LoggerFactory.getLogger(DiskFile.class).mark("Sending stop command.");
257        stopSync = true;
258                for (int i = 0; i < 29; i++) {
259                try{
260                    if(!isStopSync()) {
261                        stopSync();
262                        break;
263                    }
264                    Thread.sleep(1000L);
265                }catch(InterruptedException e){
266                    LoggerFactory.getLogger(BaseFile.class).error(e.getMessage(),e);
267                }
268                }
269                LoggerFactory.getLogger(DiskFile.class).mark("Sent stop command");
270        }
271        
272    public static boolean isStopSync(){
273            DiskFile.checkQueuePathMapping();
274            RemoteFile.checkQueuePathMapping();        
275        return stopSync && DiskFile.getQueuePathMapping().isEmpty() && RemoteFile.getQueuePathMapping().isEmpty();
276    }
277    
278    protected static boolean isDebug(RemoteFile remoteFile){
279        boolean debug = false;
280        if(remoteFile != null){
281            boolean fileDebug = remoteFile.getOrigin().getName().matches("^(\\.{0,1}DEBUG[\\.\\d\\w]*)$");
282            boolean logDeubg = remoteFile.getConfigProperties().getBoolean("LogCompletedStatus", false);
283                debug = fileDebug || logDeubg;
284        }
285        return debug || LoggerFactory.getLogger(BaseFile.class).isDebugEnabled();
286    }
287    
288    public static boolean isHidden(String path) throws Exception{
289        Path checkPath = Paths.get(path);
290
291        if(!Files.exists(checkPath)) return false;
292        
293        while(checkPath != null){
294
295            boolean hide = Files.isHidden(checkPath);
296            
297            if(hide) return true;
298            
299            checkPath = checkPath.getParent();
300            
301            
302        }
303                return false;
304    } 
305    
306    public static boolean checkSyncPathAvailable(String path, boolean showHidden,boolean showLink,long maxFileSize, List<String> syncPathAllowed,
307                        List<String> syncPathIgnored) {
308                            
309                if(maxFileSize <= 0) return false;
310                
311                try {
312                        boolean matchedAllowed = true;
313                        boolean matchedIgnored = false;
314                        Path fullPath = Paths.get(path);
315                        
316                        if(!showHidden){
317                            matchedAllowed = !isHidden(path);
318                        }
319                        
320                        if(matchedAllowed && !showLink && Files.exists(fullPath)){
321                            boolean isLink = Files.isSymbolicLink(fullPath);
322                            matchedAllowed = !isLink;
323                        }                       
324                
325                        if (matchedAllowed && syncPathAllowed != null) {
326                                for (String regex : syncPathAllowed) {
327                                        if (!CommonTools.isBlank(regex)) {
328                                                matchedAllowed = path.matches(regex);
329
330                                                if (matchedAllowed)
331                                                        break;
332                                        }
333                                }
334                        }
335                        if (matchedAllowed && syncPathIgnored != null) {
336                                for (String regex : syncPathIgnored) {
337                                        if (!CommonTools.isBlank(regex)) {
338                                                matchedIgnored = path.matches(regex);
339
340                                                if (matchedIgnored)
341                                                        break;
342                                        }
343                                }
344                        }
345
346                        boolean b = matchedAllowed && !matchedIgnored;
347                        if (b) {
348                                Path checkPath = Paths.get(path);
349                                if (Files.exists(checkPath)) {
350                                    
351                                    int parentLength = checkPath.getParent().toString().length();
352                                    int fileNamelength = checkPath.getFileName().toString().length();
353                                    
354                                    b = parentLength <= RemoteFile.MAX_FILE_PARENT_PATH_LENGTH && fileNamelength <= RemoteFile.MAX_FILE_NAME_LENGTH;
355                                    
356                                        if (b && Files.isRegularFile(checkPath)) {
357                                                long fileSize = Files.size(checkPath);
358                                                b = fileSize <= maxFileSize;
359                                        }
360                                }else{
361                                    b = false;
362                                }
363                        }
364                        
365                        if(b) {
366                        Path checkPath = Paths.get(path);
367                        if (Files.exists(checkPath)) {
368                            boolean isLink = Files.isSymbolicLink(checkPath);
369                            boolean isFile = checkPath.toFile().isFile();
370                            if(isFile && !isLink){
371                            if(RemoteFile.SYNC_CUTOFF_TIME == null){
372                                b = false;
373                            } else{
374                                long syncCutoffTimeMs = 0L;
375                                syncCutoffTimeMs = RemoteFile.SYNC_CUTOFF_TIME.getTime();
376                                        BasicFileAttributes attr = Files.readAttributes(checkPath, BasicFileAttributes.class);
377                                        FileTime fileTime = attr.lastModifiedTime();
378                                        
379                            b = fileTime.toMillis() >= syncCutoffTimeMs;
380                            }
381                            }
382                        }else{
383                            b = false;
384                        }
385                        }
386                        
387                        if (!b) {
388                                LoggerFactory.getLogger(BaseFile.class).debug("Unavailable: {}", path);
389                        }
390                        return b;
391                } catch (Exception e) {
392                        LoggerFactory.getLogger(BaseFile.class).error(e.getMessage(), e);
393                        return false;
394                }
395        }
396        
397        public boolean isDir() throws IOException {
398            return isDirectory();
399        }       
400        
401        public Timestamp timeZoneConver(String destTimeZone, String srcTimeZone, Timestamp srcDate) throws IOException {
402            try{
403            String pattern = "yyyy-MM-dd HH:mm:ss.SSS";
404                SimpleDateFormat fullFormat = new SimpleDateFormat(pattern);
405                DateTimeFormatter format = DateTimeFormatter.ofPattern(pattern);        
406                String srcDateStr = fullFormat.format(srcDate);
407                LocalDateTime ldt = LocalDateTime.parse(srcDateStr, DateTimeFormatter.ofPattern(pattern));
408                ZoneId srcZoneId = ZoneId.of(srcTimeZone);
409                ZonedDateTime srcDateTime = ldt.atZone(srcZoneId);
410                ZoneId destZoneId = ZoneId.of(destTimeZone);
411                ZonedDateTime destDateTime = srcDateTime.withZoneSameInstant(destZoneId);
412                return new Timestamp(fullFormat.parse(format.format(destDateTime)).getTime());
413            }catch(Exception e){
414               throw new IOException(e.getMessage(),e);
415            }
416        }       
417        
418        protected static String replacePath(String source){
419            return source.replaceAll("\\\\", "/");
420        }
421        
422        protected static String parsePath(String source){
423            return source.replaceAll("\\\\", "/").replaceAll("//","/");
424        }
425
426    public static boolean checkUsage(RemoteFile rf){
427        return memoryUsage() < RemoteFile.MAX_MEMORY_USAGE && rf.getDataSourceWriter().getUsage() < RemoteFile.MAX_CONNECT_USAGE && rf.getDataSourceReader().getUsage() < RemoteFile.MAX_CONNECT_USAGE;
428    }
429    
430
431    public static long memoryUsing(){
432        int mb = 1024 * 1024;
433                Runtime instance = Runtime.getRuntime();
434                long totalMemory = instance.totalMemory() / mb;
435                long freeMemory = instance.freeMemory() / mb;
436                return  (instance.totalMemory() - instance.freeMemory()) / mb;
437    }   
438
439    public static double memoryUsage(long using){
440        int mb = 1024 * 1024;
441                Runtime instance = Runtime.getRuntime();
442                long maxMemory = instance.maxMemory() / mb;
443                return (using * 1.0D / maxMemory * 1.0D);
444    }   
445    
446    public static double memoryUsage(){
447        return memoryUsage(memoryUsing());
448    }    
449    
450        protected synchronized static void debugLog(Class clazz,String point,RemoteFile rf,CacheArrayFilter filter){
451            long ctm = System.currentTimeMillis();
452            boolean canWrite = (ctm - debugLogTime) >= 60000L;
453            if(canWrite){
454                DiskFile.checkQueuePathMapping();
455                RemoteFile.checkQueuePathMapping();
456                debugLogTime = ctm;
457                long memoryUsing = memoryUsing();
458                String readerUsage = String.format("%.2f%%",rf.getDataSourceReader().getUsage()*100);
459                String writerUsage = String.format("%.2f%%",rf.getDataSourceWriter().getUsage()*100);
460                String memoryUsage = String.format("%.2f%%",memoryUsage(memoryUsing)*100);
461                LoggerFactory.getLogger(clazz).mark("************{}************",point);
462                LoggerFactory.getLogger(clazz).mark("[{}] DiskQueuePathMapping - {}",point,DiskFile.getQueuePathMapping().keySet().size());
463            LoggerFactory.getLogger(clazz).mark("[{}] RemoteQueuePathMapping - {}",point,RemoteFile.getQueuePathMapping().keySet().size());
464                LoggerFactory.getLogger(clazz).mark("[{}] CacheArrayPoolUsage - {}",point,filter.getCacheArray().getUsingPoolSize());
465                LoggerFactory.getLogger(clazz).mark("[{}] DataSourceReaderUsage - {}",point,readerUsage);       
466                LoggerFactory.getLogger(clazz).mark("[{}] DataSourceWriterUsage - {}",point,writerUsage);
467                LoggerFactory.getLogger(clazz).mark("[{}] MemoryUsage - {}mb ({})",point,memoryUsing,memoryUsage);
468            }
469        }       
470    
471}